├── .bumpversion.cfg ├── .gitattributes ├── .github └── workflows │ ├── check_optional_deps.yml │ ├── docs.yml │ ├── lint.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── anchorpy.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── cli │ └── index.md ├── clientgen │ └── index.md ├── css │ ├── custom.css │ ├── mkdocstrings.css │ └── termynal.css ├── dynamic_client │ ├── api_reference.md │ ├── comparison_with_anchor_ts.md │ ├── examples.md │ └── index.md ├── img │ ├── anchor.svg │ └── logo.png ├── index.md ├── js │ ├── custom.js │ └── termynal.js └── testing │ └── index.md ├── examples └── client-gen │ ├── basic_2 │ ├── __init__.py │ ├── accounts │ │ ├── __init__.py │ │ └── counter.py │ ├── errors │ │ ├── __init__.py │ │ └── anchor.py │ ├── instructions │ │ ├── __init__.py │ │ ├── create.py │ │ └── increment.py │ └── program_id.py │ └── tictactoe │ ├── __init__.py │ ├── accounts │ ├── __init__.py │ └── game.py │ ├── errors │ ├── __init__.py │ ├── anchor.py │ └── custom.py │ ├── instructions │ ├── __init__.py │ ├── play.py │ └── setup_game.py │ ├── program_id.py │ └── types │ ├── __init__.py │ ├── game_state.py │ ├── sign.py │ └── tile.py ├── mkdocs.yml ├── mypy.ini ├── overrides └── main.html ├── pyproject.toml ├── pytest.ini ├── src └── anchorpy │ ├── __init__.py │ ├── borsh_extension.py │ ├── cli.py │ ├── clientgen │ ├── __init__.py │ ├── accounts.py │ ├── common.py │ ├── errors.py │ ├── genpy_extension.py │ ├── instructions.py │ ├── program_id.py │ └── types.py │ ├── coder │ ├── __init__.py │ ├── accounts.py │ ├── coder.py │ ├── common.py │ ├── event.py │ ├── idl.py │ ├── instruction.py │ └── types.py │ ├── error.py │ ├── idl.py │ ├── program │ ├── __init__.py │ ├── common.py │ ├── context.py │ ├── core.py │ ├── event.py │ └── namespace │ │ ├── __init__.py │ │ ├── account.py │ │ ├── instruction.py │ │ ├── methods.py │ │ ├── rpc.py │ │ ├── simulate.py │ │ ├── transaction.py │ │ └── types.py │ ├── provider.py │ ├── py.typed │ ├── pytest_plugin.py │ ├── template.py │ ├── utils │ ├── __init__.py │ ├── rpc.py │ └── token.py │ └── workspace.py ├── tests ├── __init__.py ├── client_gen │ ├── __init__.py │ ├── example-program │ │ ├── Anchor.toml │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── programs │ │ │ └── example-program │ │ │ ├── Cargo.toml │ │ │ ├── Xargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── example_program_gen │ │ ├── __init__.py │ │ ├── accounts │ │ │ ├── __init__.py │ │ │ ├── base_account.py │ │ │ ├── my_account.py │ │ │ ├── state.py │ │ │ └── state2.py │ │ ├── errors │ │ │ ├── __init__.py │ │ │ ├── anchor.py │ │ │ └── custom.py │ │ ├── instructions │ │ │ ├── __init__.py │ │ │ ├── cause_error.py │ │ │ ├── increment_state_when_present.py │ │ │ ├── init_my_account.py │ │ │ ├── initialize.py │ │ │ ├── initialize_with_values.py │ │ │ ├── initialize_with_values2.py │ │ │ └── type_alias.py │ │ ├── program_id.py │ │ └── types │ │ │ ├── __init__.py │ │ │ ├── bar_struct.py │ │ │ ├── foo_enum.py │ │ │ ├── foo_struct.py │ │ │ └── u8_array.py │ ├── test_client_gen.py │ ├── test_functional.py │ └── token │ │ ├── __init__.py │ │ ├── accounts │ │ ├── __init__.py │ │ ├── account.py │ │ ├── mint.py │ │ └── multisig.py │ │ ├── errors │ │ ├── __init__.py │ │ ├── anchor.py │ │ └── custom.py │ │ ├── instructions │ │ ├── __init__.py │ │ ├── amount_to_ui_amount.py │ │ ├── approve.py │ │ ├── approve_checked.py │ │ ├── burn.py │ │ ├── burn_checked.py │ │ ├── close_account.py │ │ ├── freeze_account.py │ │ ├── get_account_data_size.py │ │ ├── initialize_account.py │ │ ├── initialize_account2.py │ │ ├── initialize_account3.py │ │ ├── initialize_immutable_owner.py │ │ ├── initialize_mint.py │ │ ├── initialize_mint2.py │ │ ├── initialize_multisig.py │ │ ├── initialize_multisig2.py │ │ ├── mint_to.py │ │ ├── mint_to_checked.py │ │ ├── revoke.py │ │ ├── set_authority.py │ │ ├── sync_native.py │ │ ├── thaw_account.py │ │ ├── transfer.py │ │ ├── transfer_checked.py │ │ └── ui_amount_to_amount.py │ │ ├── program_id.py │ │ └── types │ │ ├── __init__.py │ │ ├── account_state.py │ │ └── authority_type.py ├── conftest.py ├── idls │ ├── basic_0.json │ ├── basic_1.json │ ├── basic_2.json │ ├── basic_5.json │ ├── candy_machine.json │ ├── cashiers_check.json │ ├── chat.json │ ├── clientgen_example_program.json │ ├── composite.json │ ├── counter_auth.json │ ├── errors.json │ ├── escrow.json │ ├── events.json │ ├── ido_pool.json │ ├── jet.json │ ├── jet_auth.json │ ├── jet_rewards.json │ ├── merkle_distributor.json │ ├── multisig.json │ ├── puppet.json │ ├── puppet_master.json │ ├── pyth.json │ ├── quarry_mine.json │ ├── sol_cerberus.json │ ├── spl_token.json │ ├── swap.json │ ├── switchboard.json │ ├── switchboard_v2.mainnet.06022022.json │ ├── sysvars.json │ ├── tictactoe.json │ └── typescript.json ├── test_cli.py └── unit │ ├── test_accounts_array.py │ ├── test_accounts_coder.py │ ├── test_clientgen.py │ ├── test_event_parser.py │ ├── test_idl.py │ ├── test_instruction_coder.py │ ├── test_transaction.py │ └── test_types_coder.py └── uv.lock /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.21.0 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:pyproject.toml] 8 | search = version = "{current_version}" 9 | replace = version = "{new_version}" 10 | 11 | [bumpversion:file:src/anchorpy/__init__.py] 12 | search = __version__ = "{current_version}" 13 | replace = __version__ = "{new_version}" 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | anchor/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/check_optional_deps.yml: -------------------------------------------------------------------------------- 1 | name: Dependency check 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | dep_check: 10 | name: dep_check 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout main 14 | uses: actions/checkout@v2 15 | 16 | - name: Install uv 17 | uses: astral-sh/setup-uv@v5 18 | with: 19 | # Install a specific version of uv. 20 | version: "0.6.9" 21 | 22 | - name: Set up Python 23 | run: uv python install 24 | 25 | - name: Check import 26 | run: uv run python -c "from anchorpy import Program" 27 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | docs: 8 | name: Deploy docs 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout main 12 | uses: actions/checkout@v2 13 | 14 | - name: Install uv 15 | uses: astral-sh/setup-uv@v5 16 | with: 17 | # Install a specific version of uv. 18 | version: "0.6.9" 19 | 20 | - name: Set up Python 21 | run: uv python install 22 | 23 | - name: Deploy docs 24 | run: uv run mkdocs gh-deploy --force 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout main 14 | uses: actions/checkout@v2 15 | 16 | - name: Install uv 17 | uses: astral-sh/setup-uv@v5 18 | with: 19 | # Install a specific version of uv. 20 | version: "0.6.9" 21 | 22 | - name: Set up Python 23 | run: uv python install 24 | 25 | - name: Run linters 26 | run: make lint 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Install uv 12 | uses: astral-sh/setup-uv@v5 13 | with: 14 | # Install a specific version of uv. 15 | version: "0.6.9" 16 | 17 | - name: Set up Python 18 | run: uv python install 19 | 20 | - run: uv build 21 | - run: uv publish --token ${{ secrets.PYPI_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | env: 9 | solana_version: 1.18.1 10 | anchor_version: 0.29.0 11 | 12 | jobs: 13 | tests: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | node: [ 19 | "client_gen", 20 | "unit", 21 | ] 22 | steps: 23 | - name: Checkout repo. 24 | uses: actions/checkout@v2 25 | 26 | - name: Install Rust toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: 1.75.0 31 | override: true 32 | - uses: Swatinem/rust-cache@v1 33 | - name: Install Solana 34 | run: sh -c "$(curl -sSfL https://release.solana.com/v${{ env.solana_version }}/install)" 35 | 36 | - name: Add Solana to path 37 | run: echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 38 | 39 | - name: Setup node 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: '16.x' 43 | registry-url: 'https://registry.npmjs.org' 44 | 45 | - name: Install Anchor CLI 46 | run: npm install -g @coral-xyz/anchor-cli@${{ env.anchor_version }} 47 | 48 | - name: Check CLI Anchor installation 49 | run: anchor -V 50 | 51 | - name: Generate local keypair 52 | run: solana-keygen new --no-bip39-passphrase 53 | 54 | - name: Install uv 55 | uses: astral-sh/setup-uv@v5 56 | with: 57 | # Install a specific version of uv. 58 | version: "0.6.9" 59 | - name: Set up Python 60 | run: uv python install 61 | - name: Run tests 62 | run: uv run --all-extras pytest tests/${{ matrix.node }} 63 | 64 | all_good: 65 | # https://github.community/t/is-it-possible-to-require-all-github-actions-tasks-to-pass-without-enumerating-them/117957/4?u=graingert 66 | runs-on: ubuntu-latest 67 | needs: tests 68 | steps: 69 | - name: note that all tests succeeded 70 | run: echo "🎉" 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .anchor 141 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/.gitmodules -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/anchorpy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [], 6 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 7 | "unwantedRecommendations": [ 8 | "matklad.rust-analyzer" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.linting.mypyEnabled": true, 4 | "python.linting.pydocstyleEnabled": false, 5 | "files.watcherExclude": { 6 | "**/.git/objects/**": true, 7 | "**/.git/subtree-cache/**": true, 8 | "**/node_modules/*/**": true, 9 | "**/.hg/store/**": true, 10 | "anchor/**": true 11 | }, 12 | "files.exclude": { 13 | "**/.git": true, 14 | "**/.svn": true, 15 | "**/.hg": true, 16 | "**/CVS": true, 17 | "**/.DS_Store": true, 18 | "**/Thumbs.db": true, 19 | "anchor/**": true 20 | }, 21 | "rust-analyzer.files.excludeDirs": [ 22 | "anchor" 23 | ], 24 | "python.testing.pytestArgs": [ 25 | "tests" 26 | ], 27 | "python.testing.unittestEnabled": false, 28 | "python.testing.pytestEnabled": true, 29 | "editor.formatOnSave": true, 30 | "ruff.args": [ 31 | "--config=pyproject.toml" 32 | ] 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Kevin Heavey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | uv run --all-extras pytest -vv 3 | 4 | init-clientgen-examples: 5 | cd tests/client_gen/example-program/ && anchor idl parse -f programs/example-program/src/lib.rs -o ../../idls/clientgen_example_program.json && cd ../../.. && uv run anchorpy client-gen tests/idls/clientgen_example_program.json tests/client_gen/example_program_gen --program-id 3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8 --pdas 6 | uv run anchorpy client-gen tests/idls/basic_2.json examples/client-gen/basic_2 --program-id 3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8 7 | uv run anchorpy client-gen tests/idls/tictactoe.json examples/client-gen/tictactoe --program-id 3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8 8 | uv run anchorpy client-gen tests/idls/spl_token.json tests/client_gen/token --program-id TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 9 | 10 | lint: 11 | uv run ruff src tests 12 | uv run mypy src tests 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnchorPy 2 |
3 | 4 |
5 | 6 | --- 7 | 8 | [![Discord Chat](https://img.shields.io/discord/889577356681945098?color=blueviolet)](https://discord.gg/sxy4zxBckh) 9 | 10 | AnchorPy is the gateway to interacting with [Anchor](https://github.com/project-serum/anchor) programs in Python. 11 | It provides: 12 | 13 | - A static client generator 14 | - A dynamic client similar to `anchor-ts` 15 | - A Pytest plugin 16 | - A CLI with various utilities for Anchor Python development. 17 | 18 | Read the [Documentation](https://kevinheavey.github.io/anchorpy/). 19 | 20 | 21 | 22 | ## Installation (requires Python >=3.9) 23 | 24 | ```sh 25 | pip install anchorpy[cli, pytest] 26 | 27 | ``` 28 | Or, if you're not using the CLI or Pytest plugin features of AnchorPy you can just run `pip install anchorpy`. 29 | 30 | ### Development Setup 31 | 32 | If you want to contribute to AnchorPy, follow these steps to get set up: 33 | 34 | 1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) 35 | 2. Run the unit tests: 36 | ```sh 37 | uv run --all-extras pytest tests/unit 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/cli/index.md: -------------------------------------------------------------------------------- 1 | # AnchorPy CLI 2 | 3 | AnchorPy comes with a CLI to make your life easier when using Python with Anchor. 4 | 5 | 6 | ```console 7 | $ anchorpy --help 8 | Usage: anchorpy [OPTIONS] COMMAND [ARGS]... 9 | 10 | AnchorPy CLI. 11 | 12 | Options: 13 | --install-completion [bash|zsh|fish|powershell|pwsh] 14 | Install completion for the specified shell. 15 | --show-completion [bash|zsh|fish|powershell|pwsh] 16 | Show completion for the specified shell, to 17 | copy it or customize the installation. 18 | --help Show this message and exit. 19 | 20 | Commands: 21 | init Create a basic Python test file for an Anchor program. 22 | shell Start IPython shell with AnchorPy workspace object initialized. 23 | ``` 24 | 25 | ## Commands 26 | 27 | ### Init 28 | 29 | ```console 30 | $ anchorpy init --help 31 | Usage: anchorpy init [OPTIONS] PROGRAM_NAME 32 | 33 | Create a basic Python test file for an Anchor program. 34 | 35 | This does not replace `anchor init`, but rather should be run after it. 36 | 37 | The test file will live at `tests/test_$PROGRAM_NAME.py`. 38 | 39 | Arguments: 40 | PROGRAM_NAME The name of the Anchor program. [required] 41 | 42 | Options: 43 | --help Show this message and exit. 44 | ``` 45 | 46 | ### Shell 47 | 48 | ```console 49 | $ anchorpy shell --help 50 | Usage: anchorpy shell [OPTIONS] 51 | 52 | Start IPython shell with AnchorPy workspace object initialized. 53 | 54 | Note that you should run `anchor localnet` before `anchorpy shell`. 55 | 56 | Options: 57 | --help Show this message and exit. 58 | ``` 59 | 60 | #### Example 61 | 62 |
63 | 64 | ```console 65 | $ cd anchor/examples/tutorial/basic-0 && anchorpy shell 66 | Python 3.9.1 (default, Dec 11 2020, 14:32:07) 67 | Type 'copyright', 'credits' or 'license' for more information 68 | IPython 7.30.1 -- An enhanced Interactive Python. Type '?' for help. 69 | 70 | Hint: type `workspace` to see the Anchor workspace object. 71 | 72 | # In [1]:$ await workspace["basic_0"].rpc["initialize"]() 73 | Out[1]: '2q1Z8BcsSBikMLEFoeFGhUukfsNYLJx7y33rMZ57Eh4gAHJxpJ9oP9b9aFyrizh9wcuiVtAAxvmBifCXdqWeNLor' 74 | ``` 75 | 76 |
77 | 78 | ### Client-gen 79 | 80 | See [Client Generator](../clientgen) -------------------------------------------------------------------------------- /docs/css/custom.css: -------------------------------------------------------------------------------- 1 | .termynal-comment { 2 | color: #4a968f; 3 | font-style: italic; 4 | display: block; 5 | } 6 | 7 | .termy [data-termynal] { 8 | white-space: pre-wrap; 9 | font-size: 14px; 10 | overflow-wrap: break-word; 11 | } 12 | 13 | a.external-link::after { 14 | /* \00A0 is a non-breaking space 15 | to make the mark be on the same line as the link 16 | */ 17 | content: "\00A0[↪]"; 18 | } 19 | 20 | a.internal-link::after { 21 | /* \00A0 is a non-breaking space 22 | to make the mark be on the same line as the link 23 | */ 24 | content: "\00A0↪"; 25 | } -------------------------------------------------------------------------------- /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/css/termynal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * 4 | * @author Ines Montani 5 | * @version 0.0.1 6 | * @license MIT 7 | */ 8 | 9 | :root { 10 | --color-bg: #252a33; 11 | --color-text: #eee; 12 | --color-text-subtle: #a2a2a2; 13 | } 14 | 15 | [data-termynal] { 16 | width: 750px; 17 | max-width: 100%; 18 | background: var(--color-bg); 19 | color: var(--color-text); 20 | font-size: 18px; 21 | /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ 22 | font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; 23 | border-radius: 4px; 24 | padding: 75px 45px 35px; 25 | position: relative; 26 | -webkit-box-sizing: border-box; 27 | box-sizing: border-box; 28 | } 29 | 30 | [data-termynal]:before { 31 | content: ''; 32 | position: absolute; 33 | top: 15px; 34 | left: 15px; 35 | display: inline-block; 36 | width: 15px; 37 | height: 15px; 38 | border-radius: 50%; 39 | /* A little hack to display the window buttons in one pseudo element. */ 40 | background: #d9515d; 41 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 42 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 43 | } 44 | 45 | [data-termynal]:after { 46 | content: 'bash'; 47 | position: absolute; 48 | color: var(--color-text-subtle); 49 | top: 5px; 50 | left: 0; 51 | width: 100%; 52 | text-align: center; 53 | } 54 | 55 | a[data-terminal-control] { 56 | text-align: right; 57 | display: block; 58 | color: #aebbff; 59 | } 60 | 61 | [data-ty] { 62 | display: block; 63 | line-height: 2; 64 | } 65 | 66 | [data-ty]:before { 67 | /* Set up defaults and ensure empty lines are displayed. */ 68 | content: ''; 69 | display: inline-block; 70 | vertical-align: middle; 71 | } 72 | 73 | [data-ty="input"]:before, 74 | [data-ty-prompt]:before { 75 | margin-right: 0.75em; 76 | color: var(--color-text-subtle); 77 | } 78 | 79 | [data-ty="input"]:before { 80 | content: '$'; 81 | } 82 | 83 | [data-ty][data-ty-prompt]:before { 84 | content: attr(data-ty-prompt); 85 | } 86 | 87 | [data-ty-cursor]:after { 88 | content: attr(data-ty-cursor); 89 | font-family: monospace; 90 | margin-left: 0.5em; 91 | -webkit-animation: blink 1s infinite; 92 | animation: blink 1s infinite; 93 | } 94 | 95 | 96 | /* Cursor animation */ 97 | 98 | @-webkit-keyframes blink { 99 | 50% { 100 | opacity: 0; 101 | } 102 | } 103 | 104 | @keyframes blink { 105 | 50% { 106 | opacity: 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docs/dynamic_client/api_reference.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | 4 | :::anchorpy.Program 5 | :::anchorpy.Provider 6 | :::anchorpy.Context 7 | :::anchorpy.create_workspace 8 | :::anchorpy.close_workspace 9 | :::anchorpy.workspace_fixture 10 | :::anchorpy.localnet_fixture 11 | :::anchorpy.Wallet 12 | :::anchorpy.Coder 13 | :::anchorpy.InstructionCoder 14 | :::anchorpy.EventCoder 15 | :::anchorpy.AccountsCoder 16 | :::anchorpy.NamedInstruction 17 | :::anchorpy.IdlProgramAccount 18 | :::anchorpy.Event 19 | :::anchorpy.translate_address 20 | :::anchorpy.validate_accounts 21 | :::anchorpy.AccountClient 22 | :::anchorpy.ProgramAccount 23 | :::anchorpy.EventParser 24 | :::anchorpy.SimulateResponse 25 | :::anchorpy.error 26 | :::anchorpy.utils 27 | -------------------------------------------------------------------------------- /docs/dynamic_client/comparison_with_anchor_ts.md: -------------------------------------------------------------------------------- 1 | # Comparison with Typescript 2 | 3 | While AnchorPy is quite similar to the Anchor Typescript client, 4 | there are some differences: 5 | 6 | ## Dictionaries instead of objects 7 | 8 | AnchorPy tends to use dictionaries, so it uses `[key]` in some places where 9 | `anchor-ts` would use `.key`. 10 | For example, AnchorPy uses `workspace["basic_1"]` instead of `workspace.basic_1`, 11 | and `program.rpc["initialize"]()` instead of `program.rpc.initialize()` 12 | 13 | ## Explicit `Context` object 14 | 15 | AnchorPy uses a `Context` dataclass and has a `ctx` keyword argument when 16 | calling `.rpc` functions, whereas Typescript is a bit less structured. 17 | 18 | We call `program.rpc["my_func"](ctx=Context({"my_account": my_account}))` 19 | instead of `program.rpc["my_func"]({"my_account": my_account})` 20 | 21 | ## snake_case 🐍 instead of camelCase 🐪 22 | 23 | AnchorPy uses more `snake_case` to match Rust and be Pythonic. 24 | Specifically, the following names are snake-case in AnchorPy: 25 | 26 | - Workspaces: `workspace["puppet_master"]` instead of `workspace["puppetMaster"]` 27 | - Instructions: `program.rpc["my_func"]` (and `program.instruction["my_func"]`) instead of 28 | `program.rpc["myFunc"]`. 29 | - Accounts in the `ctx` arg: `{"my_account": my_account}` instead of `{"myAccount": my_account}` 30 | - Fields in user-defined types: `program.type["TransactionAccount"](is_writable=True)` instead of 31 | `program.type["TransactionAccount"](isWritable=True)` 32 | 33 | ## `program.type` namespace for user-defined types 34 | 35 | The AnchorPy `Program` object has a `.type` attribute for instantiating user-defined types. This is not present in 36 | the Typescript client. See the [examples](examples.md) for more on this. 37 | -------------------------------------------------------------------------------- /docs/dynamic_client/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Here are some of the other things you can do with AnchorPy: 4 | 5 | ## Loading a Program from an on-chain IDL 6 | 7 | If a program's IDL is stored on-chain, you can use it 8 | to initialize a program object using `Program.at`. 9 | 10 | ````python 11 | import asyncio 12 | from solana.rpc.async_api import AsyncClient 13 | from solders.pubkey import Pubkey 14 | from anchorpy import Program, Provider, Wallet 15 | 16 | 17 | async def main(): 18 | client = AsyncClient("https://api.mainnet-beta.solana.com/") 19 | provider = Provider(client, Wallet.local()) 20 | # load the Serum Swap Program (not the Serum dex itself). 21 | program_id = Pubkey.from_string("22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD") 22 | program = await Program.at( 23 | program_id, provider 24 | ) 25 | print(program.idl.name) # swap 26 | await program.close() 27 | 28 | 29 | asyncio.run(main()) 30 | 31 | 32 | ```` 33 | 34 | ## Instantiating user-defined types with `program.type` 35 | 36 | ### Enums 37 | 38 | Suppose we have an instruction that expects an Enum called `Side`, 39 | with variants `Buy` and `Sell`. The `Program` object has a `.type` 40 | namespace to make it easy to use this enum: 41 | 42 | ````python 43 | await program.rpc["my_func"](program.type["Side"].Buy()) 44 | 45 | ```` 46 | See [test_token_proxy.py](https://github.com/kevinheavey/anchorpy/blob/main/tests/test_token_proxy.py) 47 | for a more concrete example. 48 | 49 | ### Structs 50 | 51 | `.type` also allows us to build structs defined in the IDL. 52 | See this snippet from [test_multisig.py](https://github.com/kevinheavey/anchorpy/blob/main/tests/test_multisig.py): 53 | 54 | ````python 55 | program.type["TransactionAccount"]( 56 | pubkey=multisig.pubkey(), 57 | is_writable=True, 58 | is_signer=False, 59 | ) 60 | 61 | ```` 62 | 63 | ## Bulk-fetching data with `.fetch_multiple` 64 | 65 | You can use `.fetch_multiple` to get deserialized account data 66 | for many accounts at once. Look at this example from `test_misc.py`: 67 | 68 | ````python 69 | n_accounts = 200 70 | pubkeys = [initialized_keypair.pubkey()] * n_accounts # noqa: WPS435 71 | data_accounts = await program.account["Data"].fetch_multiple( 72 | pubkeys, batch_size=2 73 | ) 74 | 75 | ```` 76 | 77 | The above example fetches data for the same pubkey 200 times which is 78 | not very interesting, but it could just as easily be fetching 200 79 | different accounts. The `.fetch_multiple` method uses async batch RPC requests 80 | and `getMultipleAccounts` so it's quite efficient. 81 | 82 | !!! warning 83 | Be mindful of your RPC provider when fetching data, and plan out how 84 | many requests you'll end up sending to the RPC node. You can reliably 85 | fetch around 300 public keys in one HTTP request. 86 | -------------------------------------------------------------------------------- /docs/img/anchor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/docs/img/logo.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 |
3 | 4 |
5 | 6 | --- 7 | AnchorPy is the gateway to interacting with [Anchor](https://github.com/project-serum/anchor) programs in Python. 8 | It provides: 9 | 10 | - A [static client generator](clientgen) 11 | - A [dynamic client](dynamic_client) similar to `anchor-ts` 12 | - A [Pytest plugin](testing/#1-pytest-plugin) 13 | - A [CLI](cli) with various utilities for Anchor Python development. 14 | 15 | ## Installation (requires Python >= 3.9) 16 | 17 | ```shell 18 | pip install anchorpy[cli, pytest] 19 | ``` 20 | 21 | Or, if you're not using the CLI or Pytest plugin features of AnchorPy you can just run `pip install anchorpy`. 22 | 23 | !!! note 24 | These docs will assume you've read the [Anchor documentation](https://www.anchor-lang.com/) first. 25 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/examples/client-gen/basic_2/__init__.py -------------------------------------------------------------------------------- /examples/client-gen/basic_2/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | from .counter import Counter, CounterJSON 2 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/accounts/counter.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | from solders.pubkey import Pubkey 4 | from solana.rpc.async_api import AsyncClient 5 | from solana.rpc.commitment import Commitment 6 | import borsh_construct as borsh 7 | from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE 8 | from anchorpy.error import AccountInvalidDiscriminator 9 | from anchorpy.utils.rpc import get_multiple_accounts 10 | from anchorpy.borsh_extension import BorshPubkey 11 | from ..program_id import PROGRAM_ID 12 | 13 | 14 | class CounterJSON(typing.TypedDict): 15 | authority: str 16 | count: int 17 | 18 | 19 | @dataclass 20 | class Counter: 21 | discriminator: typing.ClassVar = b"\xff\xb0\x04\xf5\xbc\xfd|\x19" 22 | layout: typing.ClassVar = borsh.CStruct( 23 | "authority" / BorshPubkey, "count" / borsh.U64 24 | ) 25 | authority: Pubkey 26 | count: int 27 | 28 | @classmethod 29 | async def fetch( 30 | cls, 31 | conn: AsyncClient, 32 | address: Pubkey, 33 | commitment: typing.Optional[Commitment] = None, 34 | program_id: Pubkey = PROGRAM_ID, 35 | ) -> typing.Optional["Counter"]: 36 | resp = await conn.get_account_info(address, commitment=commitment) 37 | info = resp.value 38 | if info is None: 39 | return None 40 | if info.owner != program_id: 41 | raise ValueError("Account does not belong to this program") 42 | bytes_data = info.data 43 | return cls.decode(bytes_data) 44 | 45 | @classmethod 46 | async def fetch_multiple( 47 | cls, 48 | conn: AsyncClient, 49 | addresses: list[Pubkey], 50 | commitment: typing.Optional[Commitment] = None, 51 | program_id: Pubkey = PROGRAM_ID, 52 | ) -> typing.List[typing.Optional["Counter"]]: 53 | infos = await get_multiple_accounts(conn, addresses, commitment=commitment) 54 | res: typing.List[typing.Optional["Counter"]] = [] 55 | for info in infos: 56 | if info is None: 57 | res.append(None) 58 | continue 59 | if info.account.owner != program_id: 60 | raise ValueError("Account does not belong to this program") 61 | res.append(cls.decode(info.account.data)) 62 | return res 63 | 64 | @classmethod 65 | def decode(cls, data: bytes) -> "Counter": 66 | if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator: 67 | raise AccountInvalidDiscriminator( 68 | "The discriminator for this account is invalid" 69 | ) 70 | dec = Counter.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:]) 71 | return cls( 72 | authority=dec.authority, 73 | count=dec.count, 74 | ) 75 | 76 | def to_json(self) -> CounterJSON: 77 | return { 78 | "authority": str(self.authority), 79 | "count": self.count, 80 | } 81 | 82 | @classmethod 83 | def from_json(cls, obj: CounterJSON) -> "Counter": 84 | return cls( 85 | authority=Pubkey.from_string(obj["authority"]), 86 | count=obj["count"], 87 | ) 88 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/errors/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import re 3 | from solders.transaction_status import ( 4 | InstructionErrorCustom, 5 | TransactionErrorInstructionError, 6 | ) 7 | from solana.rpc.core import RPCException 8 | from solders.rpc.errors import SendTransactionPreflightFailureMessage 9 | from anchorpy.error import extract_code_and_logs 10 | from ..program_id import PROGRAM_ID 11 | from . import anchor 12 | 13 | 14 | def from_code(code: int) -> typing.Optional[anchor.AnchorError]: 15 | return anchor.from_code(code) 16 | 17 | 18 | error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)") 19 | 20 | 21 | def from_tx_error(error: RPCException) -> typing.Optional[anchor.AnchorError]: 22 | err_info = error.args[0] 23 | extracted = extract_code_and_logs(err_info, PROGRAM_ID) 24 | if extracted is None: 25 | return None 26 | return from_code(extracted[0]) 27 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | from .create import create, CreateArgs, CreateAccounts 2 | from .increment import increment, IncrementAccounts 3 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/instructions/create.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.sysvar import RENT 5 | from solders.instruction import Instruction, AccountMeta 6 | from anchorpy.borsh_extension import BorshPubkey 7 | import borsh_construct as borsh 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class CreateArgs(typing.TypedDict): 12 | authority: Pubkey 13 | 14 | 15 | layout = borsh.CStruct("authority" / BorshPubkey) 16 | 17 | 18 | class CreateAccounts(typing.TypedDict): 19 | counter: Pubkey 20 | 21 | 22 | def create( 23 | args: CreateArgs, 24 | accounts: CreateAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["counter"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 31 | ] 32 | if remaining_accounts is not None: 33 | keys += remaining_accounts 34 | identifier = b"\x18\x1e\xc8(\x05\x1c\x07w" 35 | encoded_args = layout.build( 36 | { 37 | "authority": args["authority"], 38 | } 39 | ) 40 | data = identifier + encoded_args 41 | return Instruction(program_id, data, keys) 42 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/instructions/increment.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class IncrementAccounts(typing.TypedDict): 9 | counter: Pubkey 10 | authority: Pubkey 11 | 12 | 13 | def increment( 14 | accounts: IncrementAccounts, 15 | program_id: Pubkey = PROGRAM_ID, 16 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 17 | ) -> Instruction: 18 | keys: list[AccountMeta] = [ 19 | AccountMeta(pubkey=accounts["counter"], is_signer=False, is_writable=True), 20 | AccountMeta(pubkey=accounts["authority"], is_signer=True, is_writable=False), 21 | ] 22 | if remaining_accounts is not None: 23 | keys += remaining_accounts 24 | identifier = b"\x0b\x12h\th\xae;!" 25 | encoded_args = b"" 26 | data = identifier + encoded_args 27 | return Instruction(program_id, data, keys) 28 | -------------------------------------------------------------------------------- /examples/client-gen/basic_2/program_id.py: -------------------------------------------------------------------------------- 1 | from solders.pubkey import Pubkey 2 | 3 | PROGRAM_ID = Pubkey.from_string("3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8") 4 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/examples/client-gen/tictactoe/__init__.py -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | from .game import Game, GameJSON 2 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/errors/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import re 3 | from solders.transaction_status import ( 4 | InstructionErrorCustom, 5 | TransactionErrorInstructionError, 6 | ) 7 | from solana.rpc.core import RPCException 8 | from solders.rpc.errors import SendTransactionPreflightFailureMessage 9 | from anchorpy.error import extract_code_and_logs 10 | from ..program_id import PROGRAM_ID 11 | from . import anchor 12 | from . import custom 13 | 14 | 15 | def from_code(code: int) -> typing.Union[custom.CustomError, anchor.AnchorError, None]: 16 | return custom.from_code(code) if code >= 6000 else anchor.from_code(code) 17 | 18 | 19 | error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)") 20 | 21 | 22 | def from_tx_error( 23 | error: RPCException, 24 | ) -> typing.Union[anchor.AnchorError, custom.CustomError, None]: 25 | err_info = error.args[0] 26 | extracted = extract_code_and_logs(err_info, PROGRAM_ID) 27 | if extracted is None: 28 | return None 29 | return from_code(extracted[0]) 30 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/errors/custom.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from anchorpy.error import ProgramError 3 | 4 | 5 | class TileOutOfBounds(ProgramError): 6 | def __init__(self) -> None: 7 | super().__init__(6000, None) 8 | 9 | code = 6000 10 | name = "TileOutOfBounds" 11 | msg = None 12 | 13 | 14 | class TileAlreadySet(ProgramError): 15 | def __init__(self) -> None: 16 | super().__init__(6001, None) 17 | 18 | code = 6001 19 | name = "TileAlreadySet" 20 | msg = None 21 | 22 | 23 | class GameAlreadyOver(ProgramError): 24 | def __init__(self) -> None: 25 | super().__init__(6002, None) 26 | 27 | code = 6002 28 | name = "GameAlreadyOver" 29 | msg = None 30 | 31 | 32 | class NotPlayersTurn(ProgramError): 33 | def __init__(self) -> None: 34 | super().__init__(6003, None) 35 | 36 | code = 6003 37 | name = "NotPlayersTurn" 38 | msg = None 39 | 40 | 41 | class GameAlreadyStarted(ProgramError): 42 | def __init__(self) -> None: 43 | super().__init__(6004, None) 44 | 45 | code = 6004 46 | name = "GameAlreadyStarted" 47 | msg = None 48 | 49 | 50 | CustomError = typing.Union[ 51 | TileOutOfBounds, TileAlreadySet, GameAlreadyOver, NotPlayersTurn, GameAlreadyStarted 52 | ] 53 | CUSTOM_ERROR_MAP: dict[int, CustomError] = { 54 | 6000: TileOutOfBounds(), 55 | 6001: TileAlreadySet(), 56 | 6002: GameAlreadyOver(), 57 | 6003: NotPlayersTurn(), 58 | 6004: GameAlreadyStarted(), 59 | } 60 | 61 | 62 | def from_code(code: int) -> typing.Optional[CustomError]: 63 | maybe_err = CUSTOM_ERROR_MAP.get(code) 64 | if maybe_err is None: 65 | return None 66 | return maybe_err 67 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | from .setup_game import setup_game, SetupGameArgs, SetupGameAccounts 2 | from .play import play, PlayArgs, PlayAccounts 3 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/instructions/play.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from .. import types 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class PlayArgs(typing.TypedDict): 11 | tile: types.tile.Tile 12 | 13 | 14 | layout = borsh.CStruct("tile" / types.tile.Tile.layout) 15 | 16 | 17 | class PlayAccounts(typing.TypedDict): 18 | game: Pubkey 19 | player: Pubkey 20 | 21 | 22 | def play( 23 | args: PlayArgs, 24 | accounts: PlayAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["game"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["player"], is_signer=True, is_writable=False), 31 | ] 32 | if remaining_accounts is not None: 33 | keys += remaining_accounts 34 | identifier = b"\xd5\x9d\xc1\x8e\xe48\xf8\x96" 35 | encoded_args = layout.build( 36 | { 37 | "tile": args["tile"].to_encodable(), 38 | } 39 | ) 40 | data = identifier + encoded_args 41 | return Instruction(program_id, data, keys) 42 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/instructions/setup_game.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.system_program import ID as SYS_PROGRAM_ID 5 | from solders.instruction import Instruction, AccountMeta 6 | from anchorpy.borsh_extension import BorshPubkey 7 | import borsh_construct as borsh 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class SetupGameArgs(typing.TypedDict): 12 | player_two: Pubkey 13 | 14 | 15 | layout = borsh.CStruct("player_two" / BorshPubkey) 16 | 17 | 18 | class SetupGameAccounts(typing.TypedDict): 19 | game: Pubkey 20 | player_one: Pubkey 21 | 22 | 23 | def setup_game( 24 | args: SetupGameArgs, 25 | accounts: SetupGameAccounts, 26 | program_id: Pubkey = PROGRAM_ID, 27 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 28 | ) -> Instruction: 29 | keys: list[AccountMeta] = [ 30 | AccountMeta(pubkey=accounts["game"], is_signer=True, is_writable=True), 31 | AccountMeta(pubkey=accounts["player_one"], is_signer=True, is_writable=True), 32 | AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), 33 | ] 34 | if remaining_accounts is not None: 35 | keys += remaining_accounts 36 | identifier = b"\xb4\xda\x80K:\xde#R" 37 | encoded_args = layout.build( 38 | { 39 | "player_two": args["player_two"], 40 | } 41 | ) 42 | data = identifier + encoded_args 43 | return Instruction(program_id, data, keys) 44 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/program_id.py: -------------------------------------------------------------------------------- 1 | from solders.pubkey import Pubkey 2 | 3 | PROGRAM_ID = Pubkey.from_string("3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8") 4 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/types/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from . import tile 3 | from .tile import Tile, TileJSON 4 | from . import game_state 5 | from .game_state import GameStateKind, GameStateJSON 6 | from . import sign 7 | from .sign import SignKind, SignJSON 8 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/types/game_state.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from solders.pubkey import Pubkey 5 | from anchorpy.borsh_extension import EnumForCodegen, BorshPubkey 6 | import borsh_construct as borsh 7 | 8 | 9 | class WonJSONValue(typing.TypedDict): 10 | winner: str 11 | 12 | 13 | class WonValue(typing.TypedDict): 14 | winner: Pubkey 15 | 16 | 17 | class ActiveJSON(typing.TypedDict): 18 | kind: typing.Literal["Active"] 19 | 20 | 21 | class TieJSON(typing.TypedDict): 22 | kind: typing.Literal["Tie"] 23 | 24 | 25 | class WonJSON(typing.TypedDict): 26 | value: WonJSONValue 27 | kind: typing.Literal["Won"] 28 | 29 | 30 | @dataclass 31 | class Active: 32 | discriminator: typing.ClassVar = 0 33 | kind: typing.ClassVar = "Active" 34 | 35 | @classmethod 36 | def to_json(cls) -> ActiveJSON: 37 | return ActiveJSON( 38 | kind="Active", 39 | ) 40 | 41 | @classmethod 42 | def to_encodable(cls) -> dict: 43 | return { 44 | "Active": {}, 45 | } 46 | 47 | 48 | @dataclass 49 | class Tie: 50 | discriminator: typing.ClassVar = 1 51 | kind: typing.ClassVar = "Tie" 52 | 53 | @classmethod 54 | def to_json(cls) -> TieJSON: 55 | return TieJSON( 56 | kind="Tie", 57 | ) 58 | 59 | @classmethod 60 | def to_encodable(cls) -> dict: 61 | return { 62 | "Tie": {}, 63 | } 64 | 65 | 66 | @dataclass 67 | class Won: 68 | discriminator: typing.ClassVar = 2 69 | kind: typing.ClassVar = "Won" 70 | value: WonValue 71 | 72 | def to_json(self) -> WonJSON: 73 | return WonJSON( 74 | kind="Won", 75 | value={ 76 | "winner": str(self.value["winner"]), 77 | }, 78 | ) 79 | 80 | def to_encodable(self) -> dict: 81 | return { 82 | "Won": { 83 | "winner": self.value["winner"], 84 | }, 85 | } 86 | 87 | 88 | GameStateKind = typing.Union[Active, Tie, Won] 89 | GameStateJSON = typing.Union[ActiveJSON, TieJSON, WonJSON] 90 | 91 | 92 | def from_decoded(obj: dict) -> GameStateKind: 93 | if not isinstance(obj, dict): 94 | raise ValueError("Invalid enum object") 95 | if "Active" in obj: 96 | return Active() 97 | if "Tie" in obj: 98 | return Tie() 99 | if "Won" in obj: 100 | val = obj["Won"] 101 | return Won( 102 | WonValue( 103 | winner=val["winner"], 104 | ) 105 | ) 106 | raise ValueError("Invalid enum object") 107 | 108 | 109 | def from_json(obj: GameStateJSON) -> GameStateKind: 110 | if obj["kind"] == "Active": 111 | return Active() 112 | if obj["kind"] == "Tie": 113 | return Tie() 114 | if obj["kind"] == "Won": 115 | won_json_value = typing.cast(WonJSONValue, obj["value"]) 116 | return Won( 117 | WonValue( 118 | winner=Pubkey.from_string(won_json_value["winner"]), 119 | ) 120 | ) 121 | kind = obj["kind"] 122 | raise ValueError(f"Unrecognized enum kind: {kind}") 123 | 124 | 125 | layout = EnumForCodegen( 126 | "Active" / borsh.CStruct(), 127 | "Tie" / borsh.CStruct(), 128 | "Won" / borsh.CStruct("winner" / BorshPubkey), 129 | ) 130 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/types/sign.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from anchorpy.borsh_extension import EnumForCodegen 5 | import borsh_construct as borsh 6 | 7 | 8 | class XJSON(typing.TypedDict): 9 | kind: typing.Literal["X"] 10 | 11 | 12 | class OJSON(typing.TypedDict): 13 | kind: typing.Literal["O"] 14 | 15 | 16 | @dataclass 17 | class X: 18 | discriminator: typing.ClassVar = 0 19 | kind: typing.ClassVar = "X" 20 | 21 | @classmethod 22 | def to_json(cls) -> XJSON: 23 | return XJSON( 24 | kind="X", 25 | ) 26 | 27 | @classmethod 28 | def to_encodable(cls) -> dict: 29 | return { 30 | "X": {}, 31 | } 32 | 33 | 34 | @dataclass 35 | class O: 36 | discriminator: typing.ClassVar = 1 37 | kind: typing.ClassVar = "O" 38 | 39 | @classmethod 40 | def to_json(cls) -> OJSON: 41 | return OJSON( 42 | kind="O", 43 | ) 44 | 45 | @classmethod 46 | def to_encodable(cls) -> dict: 47 | return { 48 | "O": {}, 49 | } 50 | 51 | 52 | SignKind = typing.Union[X, O] 53 | SignJSON = typing.Union[XJSON, OJSON] 54 | 55 | 56 | def from_decoded(obj: dict) -> SignKind: 57 | if not isinstance(obj, dict): 58 | raise ValueError("Invalid enum object") 59 | if "X" in obj: 60 | return X() 61 | if "O" in obj: 62 | return O() 63 | raise ValueError("Invalid enum object") 64 | 65 | 66 | def from_json(obj: SignJSON) -> SignKind: 67 | if obj["kind"] == "X": 68 | return X() 69 | if obj["kind"] == "O": 70 | return O() 71 | kind = obj["kind"] 72 | raise ValueError(f"Unrecognized enum kind: {kind}") 73 | 74 | 75 | layout = EnumForCodegen("X" / borsh.CStruct(), "O" / borsh.CStruct()) 76 | -------------------------------------------------------------------------------- /examples/client-gen/tictactoe/types/tile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from construct import Container 5 | import borsh_construct as borsh 6 | 7 | 8 | class TileJSON(typing.TypedDict): 9 | row: int 10 | column: int 11 | 12 | 13 | @dataclass 14 | class Tile: 15 | layout: typing.ClassVar = borsh.CStruct("row" / borsh.U8, "column" / borsh.U8) 16 | row: int 17 | column: int 18 | 19 | @classmethod 20 | def from_decoded(cls, obj: Container) -> "Tile": 21 | return cls(row=obj.row, column=obj.column) 22 | 23 | def to_encodable(self) -> dict[str, typing.Any]: 24 | return {"row": self.row, "column": self.column} 25 | 26 | def to_json(self) -> TileJSON: 27 | return {"row": self.row, "column": self.column} 28 | 29 | @classmethod 30 | def from_json(cls, obj: TileJSON) -> "Tile": 31 | return cls(row=obj["row"], column=obj["column"]) 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: AnchorPy 2 | theme: 3 | name: material 4 | icon: 5 | logo: material/anchor 6 | favicon: img/anchor.svg 7 | features: 8 | - navigation.tabs 9 | - navigation.top 10 | palette: 11 | - scheme: default 12 | primary: lime 13 | toggle: 14 | icon: material/toggle-switch-off-outline 15 | name: Switch to dark mode 16 | - scheme: slate 17 | toggle: 18 | icon: material/toggle-switch 19 | name: Switch to light mode 20 | custom_dir: overrides 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/kevinheavey/anchorpy 30 | repo_name: kevinheavey/anchorpy 31 | site_url: https://kevinheavey.github.io/anchorpy/ 32 | plugins: 33 | - mkdocstrings: 34 | handlers: 35 | python: 36 | selection: 37 | filters: 38 | - "!^_" # exlude all members starting with _ 39 | - "^__init__$" # but always include __init__ modules and methods 40 | rendering: 41 | show_root_heading: true 42 | show_root_full_path: false 43 | - search 44 | nav: 45 | - index.md 46 | - Client Generator: 47 | - clientgen/index.md 48 | - Dynamic Client: 49 | - dynamic_client/index.md 50 | - dynamic_client/comparison_with_anchor_ts.md 51 | - dynamic_client/examples.md 52 | - dynamic_client/api_reference.md 53 | - Testing: 54 | - testing/index.md 55 | - CLI Reference: 56 | - cli/index.md 57 | extra_css: 58 | - css/mkdocstrings.css 59 | - css/termynal.css 60 | - css/custom.css 61 | extra_javascript: 62 | - 'https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js' 63 | - 'js/termynal.js' 64 | - 'js/custom.js' -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | enable_recursive_aliases = True 3 | 4 | [mypy-pytest] 5 | ignore_missing_imports = True 6 | 7 | [mypy-xprocess] 8 | ignore_missing_imports = True 9 | 10 | [mypy-autoflake] 11 | ignore_missing_imports = True 12 | 13 | [mypy-pytest_xprocess] 14 | ignore_missing_imports = True 15 | 16 | [mypy-toolz] 17 | ignore_missing_imports = True 18 | 19 | [mypy-IPython] 20 | ignore_missing_imports = True 21 | 22 | [mypy-genpy] 23 | ignore_missing_imports = True 24 | 25 | [mypy-typer.*] 26 | ignore_missing_imports = True 27 | 28 | [mypy-black] 29 | ignore_missing_imports = True 30 | -------------------------------------------------------------------------------- /overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | {% endblock %} -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "anchorpy" 3 | version = "0.21.0" 4 | description = "The Python Anchor client." 5 | authors = [{ name = "kevinheavey", email = "kevinheavey123@gmail.com" }] 6 | requires-python = "~=3.9" 7 | readme = "README.md" 8 | dependencies = [ 9 | "construct-typing>=0.5.1,<0.6", 10 | "solana>=0.36.1,<1.0", 11 | "solders>=0.21.0,<1.0", 12 | "borsh-construct>=0.1.0,<0.2", 13 | "toolz>=0.11.2,<0.12", 14 | "pyheck>=0.1.4,<0.2", 15 | "based58>=0.1.1,<0.2", 16 | "anchorpy-core>=0.2.0,<0.3", 17 | "toml>=0.10.2,<0.11", 18 | ] 19 | 20 | [project.optional-dependencies] 21 | cli = [ 22 | "typer==0.4.1", 23 | "ipython>=8.0.1,<9", 24 | "genpy~=2021.1", 25 | "black>=22.3.0,<23", 26 | "autoflake~=1.4", 27 | ] 28 | pytest = [ 29 | "pytest>=7.2.0,<8", 30 | "py>=1.11.0,<2", 31 | "pytest-xprocess>=0.18.1,<0.19", 32 | "pytest-asyncio>=0.21.0,<0.22", 33 | ] 34 | 35 | [project.urls] 36 | Repository = "https://github.com/kevinheavey/anchorpy" 37 | Documentation = "https://kevinheavey.github.io/anchorpy/" 38 | 39 | [project.scripts] 40 | anchorpy = "anchorpy.cli:app" 41 | 42 | [project.entry-points.pytest11] 43 | pytest_anchorpy = "anchorpy.pytest_plugin" 44 | 45 | [dependency-groups] 46 | dev = [ 47 | "black>=22.3.0,<23", 48 | "mypy>=0.982,<0.983", 49 | "jinja2==3.0.3", 50 | "mkdocs-material>=8.1.7,<9", 51 | "bump2version>=1.0.1,<2", 52 | "mkdocstrings>=0.17.0,<0.18", 53 | "py>=1.11.0,<2", 54 | "pytest>=7.2.0,<8", 55 | "pytest-asyncio>=0.21.0,<0.22", 56 | "pytest-xprocess>=0.18.1,<0.19", 57 | "ruff>=0.0.220,<0.0.221", 58 | ] 59 | 60 | [build-system] 61 | requires = ["hatchling"] 62 | build-backend = "hatchling.build" 63 | 64 | [tool.ruff] 65 | select = [ 66 | "A", 67 | "B", 68 | "D", 69 | "E", 70 | "F", 71 | "I", 72 | "ARG", 73 | "BLE", 74 | "C4", 75 | "SIM", 76 | "PLC", 77 | "PLE", 78 | "PLR", 79 | "PLW", 80 | "RUF", 81 | ] 82 | ignore = ["B023", "D100", "D101", "D102", "D103", "D104", "D107", "D203"] 83 | 84 | # Exclude a variety of commonly ignored directories. 85 | exclude = [ 86 | ".bzr", 87 | ".direnv", 88 | ".eggs", 89 | ".git", 90 | ".hg", 91 | ".mypy_cache", 92 | ".nox", 93 | ".pants.d", 94 | ".ruff_cache", 95 | ".svn", 96 | ".tox", 97 | ".venv", 98 | "__pypackages__", 99 | "_build", 100 | "buck-out", 101 | "build", 102 | "dist", 103 | "node_modules", 104 | "venv", 105 | ] 106 | 107 | # Allow unused variables when underscore-prefixed. 108 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 109 | 110 | # Assume Python 3.10. 111 | target-version = "py310" 112 | 113 | [tool.ruff.pydocstyle] 114 | convention = "google" 115 | 116 | [tool.ruff.per-file-ignores] 117 | "src/anchorpy/__init__.py" = ["F401"] 118 | "src/anchorpy/coder/*.py" = ["ARG002"] 119 | "src/anchorpy/borsh_extension.py" = ["ARG002"] 120 | "tests/**/*.py" = ["ARG001", "E501"] 121 | "tests/client_gen/example_program_gen/**/*.py" = ["C417", "I001", "F401"] 122 | "tests/client_gen/token/**/*.py" = ["C417", "I001", "F401"] 123 | "src/anchorpy/cli.py" = ["B008"] 124 | 125 | [tool.pyright] 126 | reportMissingModuleSource = false 127 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | unit: mark a test as a unit test. 4 | addopts = -x 5 | asyncio_mode=strict 6 | -------------------------------------------------------------------------------- /src/anchorpy/__init__.py: -------------------------------------------------------------------------------- 1 | """The Python Anchor client.""" 2 | from contextlib import suppress as __suppress 3 | 4 | from anchorpy_core.idl import Idl 5 | 6 | from anchorpy import error, utils 7 | from anchorpy.coder.coder import AccountsCoder, Coder, EventCoder, InstructionCoder 8 | from anchorpy.idl import IdlProgramAccount 9 | from anchorpy.program.common import ( 10 | Event, 11 | NamedInstruction, 12 | translate_address, 13 | validate_accounts, 14 | ) 15 | from anchorpy.program.context import Context 16 | from anchorpy.program.core import Program 17 | from anchorpy.program.event import EventParser 18 | from anchorpy.program.namespace.account import AccountClient, ProgramAccount 19 | from anchorpy.program.namespace.simulate import SimulateResponse 20 | from anchorpy.provider import Provider, Wallet 21 | from anchorpy.workspace import WorkspaceType, close_workspace, create_workspace 22 | 23 | __has_pytest = False 24 | with __suppress(ImportError): 25 | from anchorpy.pytest_plugin import ( 26 | localnet_fixture, 27 | workspace_fixture, 28 | ) 29 | 30 | __has_pytest = True 31 | 32 | __all_core = [ 33 | "Program", 34 | "Provider", 35 | "Context", 36 | "create_workspace", 37 | "close_workspace", 38 | "Idl", 39 | "WorkspaceType", 40 | "Wallet", 41 | "Coder", 42 | "InstructionCoder", 43 | "EventCoder", 44 | "AccountsCoder", 45 | "NamedInstruction", 46 | "IdlProgramAccount", 47 | "Event", 48 | "translate_address", 49 | "validate_accounts", 50 | "AccountClient", 51 | "ProgramAccount", 52 | "EventParser", 53 | "SimulateResponse", 54 | "error", 55 | "utils", 56 | ] 57 | 58 | __all__ = ( 59 | [ 60 | *__all_core, 61 | "localnet_fixture", 62 | "workspace_fixture", 63 | ] 64 | if __has_pytest 65 | else __all_core 66 | ) 67 | 68 | __version__ = "0.21.0" 69 | -------------------------------------------------------------------------------- /src/anchorpy/clientgen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/src/anchorpy/clientgen/__init__.py -------------------------------------------------------------------------------- /src/anchorpy/clientgen/program_id.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from black import FileMode, format_str 4 | from genpy import Assign, Collection, FromImport 5 | 6 | 7 | def gen_program_id_code(program_id: str) -> str: 8 | import_line = FromImport("solders.pubkey", ["Pubkey"]) 9 | assignment_line = Assign("PROGRAM_ID", f'Pubkey.from_string("{program_id}")') 10 | return str(Collection([import_line, assignment_line])) 11 | 12 | 13 | def gen_program_id(program_id: str, root: Path) -> None: 14 | code = gen_program_id_code(program_id) 15 | formatted = format_str(code, mode=FileMode()) 16 | (root / "program_id.py").write_text(formatted) 17 | -------------------------------------------------------------------------------- /src/anchorpy/coder/__init__.py: -------------------------------------------------------------------------------- 1 | """This subpackage defines the Coder class.""" 2 | -------------------------------------------------------------------------------- /src/anchorpy/coder/accounts.py: -------------------------------------------------------------------------------- 1 | """This module provides `AccountsCoder` and `_account_discriminator`.""" 2 | from hashlib import sha256 3 | from typing import Any, Tuple 4 | 5 | from anchorpy_core.idl import Idl 6 | from construct import Adapter, Bytes, Container, Sequence, Switch 7 | 8 | from anchorpy.coder.idl import _typedef_layout 9 | from anchorpy.program.common import NamedInstruction as AccountToSerialize 10 | 11 | ACCOUNT_DISCRIMINATOR_SIZE = 8 # bytes 12 | 13 | 14 | class AccountsCoder(Adapter): 15 | """Encodes and decodes account data.""" 16 | 17 | def __init__(self, idl: Idl) -> None: 18 | """Init. 19 | 20 | Args: 21 | idl: The parsed IDL object. 22 | """ 23 | self._accounts_layout = { 24 | acc.name: _typedef_layout(acc, idl.types, acc.name) for acc in idl.accounts 25 | } 26 | self.acc_name_to_discriminator = { 27 | acc.name: _account_discriminator(acc.name) for acc in idl.accounts 28 | } 29 | self.discriminator_to_acc_name = { 30 | disc: acc_name for acc_name, disc in self.acc_name_to_discriminator.items() 31 | } 32 | discriminator_to_typedef_layout = { 33 | disc: self._accounts_layout[acc_name] 34 | for acc_name, disc in self.acc_name_to_discriminator.items() 35 | } 36 | subcon = Sequence( 37 | "discriminator" / Bytes(ACCOUNT_DISCRIMINATOR_SIZE), 38 | Switch(lambda this: this.discriminator, discriminator_to_typedef_layout), 39 | ) 40 | super().__init__(subcon) # type: ignore 41 | 42 | def decode(self, obj: bytes) -> Container[Any]: 43 | """Decode account data. 44 | 45 | Args: 46 | obj: Data to decode. 47 | 48 | Returns: 49 | Decoded data. 50 | """ 51 | return self.parse(obj).data 52 | 53 | def _decode(self, obj: Tuple[bytes, Any], context, path) -> AccountToSerialize: 54 | return AccountToSerialize( 55 | data=obj[1], 56 | name=self.discriminator_to_acc_name[obj[0]], 57 | ) 58 | 59 | def _encode(self, obj: AccountToSerialize, context, path) -> Tuple[bytes, Any]: 60 | discriminator = self.acc_name_to_discriminator[obj.name] 61 | return discriminator, obj.data 62 | 63 | 64 | def _account_discriminator(name: str) -> bytes: 65 | """Calculate unique 8 byte discriminator prepended to all anchor accounts. 66 | 67 | Args: 68 | name: The account name. 69 | 70 | Returns: 71 | The discriminator in bytes. 72 | """ 73 | return sha256(f"account:{name}".encode()).digest()[:ACCOUNT_DISCRIMINATOR_SIZE] 74 | -------------------------------------------------------------------------------- /src/anchorpy/coder/coder.py: -------------------------------------------------------------------------------- 1 | """Provides the Coder class.""" 2 | from anchorpy_core.idl import Idl 3 | 4 | from anchorpy.coder.accounts import AccountsCoder 5 | from anchorpy.coder.event import EventCoder 6 | from anchorpy.coder.instruction import InstructionCoder 7 | from anchorpy.coder.types import TypesCoder 8 | 9 | 10 | class Coder: 11 | """Coder provides a facade for encoding and decoding all IDL related objects.""" 12 | 13 | def __init__(self, idl: Idl): 14 | """Initialize the coder. 15 | 16 | Args: 17 | idl: a parsed Idl instance. 18 | """ 19 | self.instruction: InstructionCoder = InstructionCoder(idl) 20 | self.accounts: AccountsCoder = AccountsCoder(idl) 21 | self.events: EventCoder = EventCoder(idl) 22 | self.types: TypesCoder = TypesCoder(idl) 23 | -------------------------------------------------------------------------------- /src/anchorpy/coder/common.py: -------------------------------------------------------------------------------- 1 | """Common utilities for encoding and decoding.""" 2 | from hashlib import sha256 3 | from typing import Dict, Union 4 | 5 | from anchorpy_core.idl import ( 6 | Idl, 7 | IdlEnumVariant, 8 | IdlField, 9 | IdlType, 10 | IdlTypeArray, 11 | IdlTypeCompound, 12 | IdlTypeDefined, 13 | IdlTypeDefinition, 14 | IdlTypeDefinitionTyAlias, 15 | IdlTypeDefinitionTyEnum, 16 | IdlTypeOption, 17 | IdlTypeSimple, 18 | IdlTypeVec, 19 | ) 20 | 21 | 22 | def _sighash(ix_name: str) -> bytes: 23 | """Not technically sighash, since we don't include the arguments. 24 | 25 | (Because Rust doesn't allow function overloading.) 26 | 27 | Args: 28 | ix_name: The instruction name. 29 | 30 | Returns: 31 | The sighash bytes. 32 | """ 33 | formatted_str = f"global:{ix_name}" 34 | return sha256(formatted_str.encode()).digest()[:8] 35 | 36 | 37 | def _type_size_compound_type(idl: Idl, ty: IdlTypeCompound) -> int: 38 | if isinstance(ty, IdlTypeVec): 39 | return 1 40 | if isinstance(ty, IdlTypeOption): 41 | return 1 + _type_size(idl, ty.option) 42 | if isinstance(ty, IdlTypeDefined): 43 | defined = ty.defined 44 | filtered = [t for t in idl.types if t.name == defined] 45 | if len(filtered) != 1: 46 | raise ValueError(f"Type not found {ty}") 47 | type_def = filtered[0] 48 | return _account_size(idl, type_def) 49 | if isinstance(ty, IdlTypeArray): 50 | element_type = ty.array[0] 51 | array_size = ty.array[1] 52 | return _type_size(idl, element_type) * array_size 53 | raise ValueError(f"type_size not implemented for {ty}") 54 | 55 | 56 | def _type_size(idl: Idl, ty: IdlType) -> int: 57 | """Return the size of the type in bytes. 58 | 59 | For variable length types, just return 1. 60 | Users should override this value in such cases. 61 | 62 | Args: 63 | idl: The parsed `Idl` object. 64 | ty: The type object from the IDL. 65 | 66 | Returns: 67 | The size of the object in bytes. 68 | """ 69 | sizes: Dict[IdlTypeSimple, int] = { 70 | IdlTypeSimple.Bool: 1, 71 | IdlTypeSimple.U8: 1, 72 | IdlTypeSimple.I8: 1, 73 | IdlTypeSimple.Bytes: 1, 74 | IdlTypeSimple.String: 1, 75 | IdlTypeSimple.I16: 2, 76 | IdlTypeSimple.U16: 2, 77 | IdlTypeSimple.U32: 4, 78 | IdlTypeSimple.I32: 4, 79 | IdlTypeSimple.F32: 4, 80 | IdlTypeSimple.U64: 8, 81 | IdlTypeSimple.I64: 8, 82 | IdlTypeSimple.F64: 8, 83 | IdlTypeSimple.U128: 16, 84 | IdlTypeSimple.I128: 16, 85 | IdlTypeSimple.PublicKey: 32, 86 | } 87 | if isinstance(ty, IdlTypeSimple): 88 | return sizes[ty] 89 | return _type_size_compound_type(idl, ty) 90 | 91 | 92 | def _variant_field_size(idl: Idl, field: Union[IdlField, IdlType]) -> int: 93 | if isinstance(field, IdlField): 94 | return _type_size(idl, field.ty) 95 | return _type_size(idl, field) 96 | 97 | 98 | def _variant_size(idl: Idl, variant: IdlEnumVariant) -> int: 99 | if variant.fields is None: 100 | return 0 101 | field_sizes = [] 102 | field: Union[IdlField, IdlType] 103 | for field in variant.fields.fields: 104 | field_sizes.append(_variant_field_size(idl, field)) 105 | return sum(field_sizes) 106 | 107 | 108 | def _account_size(idl: Idl, idl_account: IdlTypeDefinition) -> int: 109 | """Calculate account size in bytes. 110 | 111 | Args: 112 | idl: The parsed `Idl` instance. 113 | idl_account: An item from `idl.accounts`. 114 | 115 | Returns: 116 | Account size. 117 | """ 118 | idl_account_type = idl_account.ty 119 | if isinstance(idl_account_type, IdlTypeDefinitionTyEnum): 120 | variant_sizes = ( 121 | _variant_size(idl, variant) for variant in idl_account_type.variants 122 | ) 123 | return max(variant_sizes) + 1 124 | if isinstance(idl_account_type, IdlTypeDefinitionTyAlias): 125 | return _type_size(idl, idl_account_type.value) 126 | if idl_account_type.fields is None: 127 | return 0 128 | return sum(_type_size(idl, f.ty) for f in idl_account_type.fields) 129 | -------------------------------------------------------------------------------- /src/anchorpy/coder/event.py: -------------------------------------------------------------------------------- 1 | """This module deals with (de)serializing Anchor events.""" 2 | from hashlib import sha256 3 | from typing import Any, Dict, Optional, Tuple 4 | 5 | from anchorpy_core.idl import ( 6 | Idl, 7 | IdlEvent, 8 | IdlField, 9 | IdlTypeDefinition, 10 | IdlTypeDefinitionTyStruct, 11 | ) 12 | from construct import Adapter, Bytes, Construct, Sequence, Switch 13 | from pyheck import snake 14 | 15 | from anchorpy.coder.idl import _typedef_layout 16 | from anchorpy.program.common import Event 17 | 18 | 19 | def _event_discriminator(name: str) -> bytes: 20 | """Get 8-byte discriminator from event name. 21 | 22 | Args: 23 | name: The event name. 24 | 25 | Returns: 26 | Discriminator 27 | """ 28 | return sha256(f"event:{name}".encode()).digest()[:8] 29 | 30 | 31 | def _event_layout(event: IdlEvent, idl: Idl) -> Construct: 32 | event_type_def = IdlTypeDefinition( 33 | name=event.name, 34 | docs=None, 35 | ty=IdlTypeDefinitionTyStruct( 36 | fields=[ 37 | IdlField(name=snake(f.name), docs=None, ty=f.ty) for f in event.fields 38 | ], 39 | ), 40 | ) 41 | return _typedef_layout(event_type_def, idl.types, event.name) 42 | 43 | 44 | class EventCoder(Adapter): 45 | """Encodes and decodes Anchor events.""" 46 | 47 | def __init__(self, idl: Idl): 48 | """Initialize the EventCoder. 49 | 50 | Args: 51 | idl: The parsed Idl object. 52 | """ 53 | self.idl = idl 54 | idl_events = idl.events 55 | layouts: Dict[str, Construct] 56 | if idl_events: 57 | layouts = {event.name: _event_layout(event, idl) for event in idl_events} 58 | else: 59 | layouts = {} 60 | self.layouts = layouts 61 | self.discriminators: Dict[bytes, str] = ( 62 | {} 63 | if idl_events is None 64 | else {_event_discriminator(event.name): event.name for event in idl_events} 65 | ) 66 | self.discriminator_to_layout = { 67 | disc: self.layouts[event_name] 68 | for disc, event_name in self.discriminators.items() 69 | } 70 | subcon = Sequence( 71 | "discriminator" / Bytes(8), # not base64-encoded here 72 | Switch(lambda this: this.discriminator, self.discriminator_to_layout), 73 | ) 74 | super().__init__(subcon) # type: ignore 75 | 76 | def _decode(self, obj: Tuple[bytes, Any], context, path) -> Optional[Event]: 77 | disc = obj[0] 78 | try: 79 | event_name = self.discriminators[disc] 80 | except KeyError: 81 | return None 82 | return Event(data=obj[1], name=event_name) 83 | -------------------------------------------------------------------------------- /src/anchorpy/coder/instruction.py: -------------------------------------------------------------------------------- 1 | """This module deals (de)serializing program instructions.""" 2 | from typing import Any, Dict, Protocol, Tuple, TypeVar, cast 3 | 4 | from anchorpy_core.idl import Idl 5 | from borsh_construct import CStruct 6 | from construct import Adapter, Bytes, Construct, Container, Sequence, Switch 7 | from pyheck import snake 8 | 9 | from anchorpy.coder.common import _sighash 10 | from anchorpy.coder.idl import _field_layout 11 | from anchorpy.idl import TypeDefs 12 | from anchorpy.program.common import NamedInstruction 13 | 14 | 15 | class _Sighash(Adapter): 16 | """Sighash as a Construct Adapter.""" 17 | 18 | def __init__(self) -> None: 19 | """Initialize.""" 20 | super().__init__(Bytes(8)) # type: ignore 21 | 22 | def _encode(self, obj: str, context, path) -> bytes: 23 | return _sighash(obj) 24 | 25 | def _decode(self, obj: bytes, context, path): 26 | raise ValueError("Sighash cannot be reversed") 27 | 28 | 29 | class InstructionCoder(Adapter): 30 | """Encodes and decodes program instructions.""" 31 | 32 | def __init__(self, idl: Idl) -> None: 33 | """Init. 34 | 35 | Args: 36 | idl: The parsed IDL object. 37 | """ 38 | self.ix_layout = _parse_ix_layout(idl) 39 | sighasher = _Sighash() 40 | sighash_layouts: Dict[bytes, Construct] = {} 41 | sighashes: Dict[str, bytes] = {} 42 | sighash_to_name: Dict[bytes, str] = {} 43 | for ix in idl.instructions: 44 | ix_name = snake(ix.name) 45 | sh = sighasher.build(ix_name) 46 | sighashes[ix_name] = sh 47 | sighash_layouts[sh] = self.ix_layout[ix_name] 48 | sighash_to_name[sh] = ix_name 49 | self.sighash_layouts = sighash_layouts 50 | self.sighashes = sighashes 51 | self.sighash_to_name = sighash_to_name 52 | subcon = Sequence( 53 | "sighash" / Bytes(8), 54 | Switch(lambda this: this.sighash, sighash_layouts), 55 | ) 56 | super().__init__(subcon) # type: ignore 57 | 58 | def encode(self, ix_name: str, ix: Dict[str, Any]) -> bytes: 59 | """Encode a program instruction. 60 | 61 | Args: 62 | ix_name: The name of the instruction 63 | ix: The data to encode. 64 | 65 | Returns: 66 | The encoded instruction. 67 | """ 68 | return self.build(NamedInstruction(name=ix_name, data=ix)) 69 | 70 | def _decode(self, obj: Tuple[bytes, Any], context, path) -> NamedInstruction: 71 | return NamedInstruction(data=obj[1], name=self.sighash_to_name[obj[0]]) 72 | 73 | def _encode( 74 | self, obj: NamedInstruction, context: Container, path 75 | ) -> Tuple[bytes, Any]: 76 | return (self.sighashes[obj.name], obj.data) 77 | 78 | 79 | _SA = TypeVar("_SA", bound="_SupportsAdd") 80 | 81 | 82 | class _SupportsAdd(Protocol): 83 | """Any type T where +(:T, :T) -> T.""" 84 | 85 | def __add__(self: _SA, other: _SA) -> _SA: 86 | ... 87 | 88 | 89 | def _parse_ix_layout(idl: Idl) -> Dict[str, Construct]: 90 | ix_layout: Dict[str, Construct] = {} 91 | for ix in idl.instructions: 92 | typedefs = cast(_SupportsAdd, idl.accounts) + cast(_SupportsAdd, idl.types) 93 | field_layouts = [ 94 | _field_layout(arg, cast(TypeDefs, typedefs)) for arg in ix.args 95 | ] 96 | ix_name = snake(ix.name) 97 | ix_layout[ix_name] = ix_name / CStruct(*field_layouts) 98 | return ix_layout 99 | -------------------------------------------------------------------------------- /src/anchorpy/coder/types.py: -------------------------------------------------------------------------------- 1 | """The TypesCoder class is for encoding and decoding user-defined types.""" 2 | 3 | from typing import Any, Dict 4 | 5 | from anchorpy_core.idl import Idl 6 | from construct import Construct, Container 7 | 8 | from anchorpy.coder.idl import _typedef_layout_without_field_name 9 | 10 | 11 | class TypesCoder: 12 | """Encodes and decodes user-defined types in Anchor programs.""" 13 | 14 | def __init__(self, idl: Idl) -> None: 15 | """Initialize the TypesCoder. 16 | 17 | Args: 18 | idl: The parsed IDL object. 19 | """ 20 | self.idl = idl 21 | self.types_layouts: Dict[str, Construct] = {} 22 | 23 | self.filtered_types = [] 24 | if idl.types: 25 | self.filtered_types = [ 26 | ty for ty in idl.types if not getattr(ty, "generics", None) 27 | ] 28 | 29 | def _get_layout(self, name: str) -> Construct: 30 | """Get or create a layout for a given type name. 31 | 32 | Args: 33 | name: The name of the type. 34 | 35 | Returns: 36 | The construct layout for the type. 37 | 38 | Raises: 39 | ValueError: If the type is not found. 40 | """ 41 | if name in self.types_layouts: 42 | return self.types_layouts[name] 43 | 44 | type_defs = [t for t in self.filtered_types if t.name == name] 45 | if not type_defs: 46 | raise ValueError(f"Unknown type: {name}") 47 | 48 | layout = _typedef_layout_without_field_name(type_defs[0], self.idl.types) 49 | self.types_layouts[name] = layout 50 | return layout 51 | 52 | def encode(self, name: str, data: Any) -> bytes: 53 | """Encode a user-defined type. 54 | 55 | Args: 56 | name: The name of the type. 57 | data: The data to encode. 58 | 59 | Returns: 60 | The encoded data. 61 | 62 | Raises: 63 | ValueError: If the type is not found. 64 | """ 65 | layout = self._get_layout(name) 66 | return layout.build(data) 67 | 68 | def decode(self, name: str, buffer: bytes) -> Container[Any]: 69 | """Decode a user-defined type. 70 | 71 | Args: 72 | name: The name of the type. 73 | buffer: The buffer to decode. 74 | 75 | Returns: 76 | The decoded data. 77 | 78 | Raises: 79 | ValueError: If the type is not found. 80 | """ 81 | layout = self._get_layout(name) 82 | return layout.parse(buffer) 83 | -------------------------------------------------------------------------------- /src/anchorpy/idl.py: -------------------------------------------------------------------------------- 1 | """Contains code for parsing the IDL file.""" 2 | from typing import Sequence, TypedDict 3 | 4 | import solders.pubkey 5 | from anchorpy_core.idl import IdlTypeDefinition 6 | from borsh_construct import U8, CStruct, Vec 7 | 8 | from anchorpy.borsh_extension import BorshPubkey 9 | 10 | 11 | def _idl_address(program_id: solders.pubkey.Pubkey) -> solders.pubkey.Pubkey: 12 | """Deterministic IDL address as a function of the program id. 13 | 14 | Args: 15 | program_id: The program ID. 16 | 17 | Returns: 18 | The public key of the IDL. 19 | """ 20 | base = solders.pubkey.Pubkey.find_program_address([], program_id)[0] 21 | return solders.pubkey.Pubkey.create_with_seed(base, "anchor:idl", program_id) 22 | 23 | 24 | class IdlProgramAccount(TypedDict): 25 | """The on-chain account of the IDL.""" 26 | 27 | authority: solders.pubkey.Pubkey 28 | data: bytes 29 | 30 | 31 | IDL_ACCOUNT_LAYOUT = CStruct("authority" / BorshPubkey, "data" / Vec(U8)) 32 | 33 | 34 | def _decode_idl_account(data: bytes) -> IdlProgramAccount: 35 | """Decode on-chain IDL. 36 | 37 | Args: 38 | data: binary data from the account that stores the IDL. 39 | 40 | Returns: 41 | Decoded IDL. 42 | """ 43 | return IDL_ACCOUNT_LAYOUT.parse(data) 44 | 45 | 46 | TypeDefs = Sequence[IdlTypeDefinition] 47 | -------------------------------------------------------------------------------- /src/anchorpy/program/__init__.py: -------------------------------------------------------------------------------- 1 | """This subpacakge defines the `Program` class.""" 2 | -------------------------------------------------------------------------------- /src/anchorpy/program/common.py: -------------------------------------------------------------------------------- 1 | """Common utilities.""" 2 | from dataclasses import dataclass 3 | from typing import Any, Dict, NamedTuple, Tuple, Union, cast 4 | 5 | from anchorpy_core.idl import ( 6 | IdlAccountItem, 7 | IdlAccounts, 8 | IdlInstruction, 9 | ) 10 | from construct import Container 11 | from pyheck import snake 12 | from solders.pubkey import Pubkey 13 | 14 | from anchorpy.program.context import Accounts 15 | 16 | AddressType = Union[Pubkey, str] 17 | 18 | 19 | class Event(NamedTuple): 20 | """A parsed event object.""" 21 | 22 | name: str 23 | data: Any 24 | 25 | 26 | @dataclass 27 | class NamedInstruction: 28 | """Container for a named instruction. 29 | 30 | Attributes: 31 | data: The actual instruction data. 32 | name: The name of the instruction. 33 | """ 34 | 35 | data: Union[Dict[str, Any], Container[Any]] 36 | name: str 37 | 38 | 39 | def _to_instruction(idl_ix: IdlInstruction, args: Tuple) -> NamedInstruction: 40 | """Convert an IDL instruction and arguments to an Instruction object. 41 | 42 | Args: 43 | idl_ix: The IDL instruction object. 44 | args: The instruction arguments. 45 | 46 | Raises: 47 | ValueError: If the incorrect number of arguments is provided. 48 | 49 | Returns: 50 | The parsed Instruction object. 51 | """ 52 | if len(idl_ix.args) != len(args): 53 | raise ValueError("Invalid argument length") 54 | ix: Dict[str, Any] = {} 55 | for idx, ix_arg in enumerate(idl_ix.args): 56 | ix[snake(ix_arg.name)] = args[idx] 57 | return NamedInstruction(data=ix, name=snake(idl_ix.name)) 58 | 59 | 60 | def validate_accounts(ix_accounts: list[IdlAccountItem], accounts: Accounts): 61 | """Check that accounts passed in `ctx` match the IDL. 62 | 63 | Args: 64 | ix_accounts: Accounts from the IDL. 65 | accounts: Accounts from the `ctx` arg. 66 | 67 | Raises: 68 | ValueError: If `ctx` accounts don't match the IDL. 69 | """ 70 | for acc in ix_accounts: 71 | acc_name = snake(acc.name) 72 | if isinstance(acc, IdlAccounts): 73 | nested = cast(Accounts, accounts[acc_name]) 74 | validate_accounts(acc.accounts, nested) 75 | elif acc_name not in accounts: 76 | raise ValueError(f"Invalid arguments: {acc_name} not provided") 77 | 78 | 79 | def translate_address(address: AddressType) -> Pubkey: 80 | """Convert `str | Pubkey` into `Pubkey`. 81 | 82 | Args: 83 | address: Public key as string or `Pubkey`. 84 | 85 | Returns: 86 | Public key as `Pubkey`. 87 | """ 88 | if isinstance(address, str): 89 | return Pubkey.from_string(address) 90 | return address 91 | -------------------------------------------------------------------------------- /src/anchorpy/program/context.py: -------------------------------------------------------------------------------- 1 | """This module contains code handling the Context object.""" 2 | from dataclasses import dataclass, field 3 | from typing import Any, Dict, List, Optional, Tuple 4 | 5 | from anchorpy_core.idl import IdlInstruction 6 | from pyheck import snake 7 | from solana.rpc.types import TxOpts 8 | from solders.instruction import AccountMeta, Instruction 9 | from solders.keypair import Keypair 10 | 11 | from anchorpy.error import ArgsError 12 | 13 | # should be Dict[str, Union[Pubkey, Accounts]] 14 | # but mypy doesn't support recursive types 15 | Accounts = Dict[str, Any] 16 | 17 | 18 | @dataclass 19 | class Context: 20 | """Context provides all non-argument inputs for generating Anchor transactions. 21 | 22 | Attributes: 23 | accounts: The accounts used in the instruction context. 24 | remaining_accounts: All accounts to pass into an instruction *after* the main 25 | `accounts`. This can be used for optional or otherwise unknown accounts. 26 | signers: Accounts that must sign a given transaction. 27 | pre_instructions: Instructions to run *before* a given method. Often this is 28 | used, for example to create accounts prior to executing a method. 29 | post_instructions: Instructions to run *after* a given method. Often this is 30 | used, for example to close accounts prior to executing a method. 31 | options: Commitment parameters to use for a transaction. 32 | 33 | """ 34 | 35 | # For some reason mkdocstrings doesn't understand the full type hint 36 | # here if we use list[Instruction] instead of typing.List. 37 | # Weirdly there are other places where it understands list[whatever]. 38 | 39 | accounts: Accounts = field(default_factory=dict) 40 | remaining_accounts: List[AccountMeta] = field(default_factory=list) 41 | signers: List[Keypair] = field(default_factory=list) 42 | pre_instructions: List[Instruction] = field(default_factory=list) 43 | post_instructions: List[Instruction] = field(default_factory=list) 44 | options: Optional[TxOpts] = None 45 | 46 | 47 | def _check_args_length( 48 | idl_ix: IdlInstruction, 49 | args: Tuple, 50 | ) -> None: 51 | """Check that the correct number of args is passed to the RPC function. 52 | 53 | Args: 54 | idl_ix: The IDL instruction object. 55 | args: The instruction arguments. 56 | 57 | Raises: 58 | ArgsError: If the correct number of args is not parsed. 59 | """ 60 | if len(args) != len(idl_ix.args): 61 | expected_arg_names = [snake(arg.name) for arg in idl_ix.args] 62 | raise ArgsError( 63 | f"Provided incorrect number of args to instruction={snake(idl_ix.name)}. " 64 | f"Expected {expected_arg_names}", 65 | f"Received {args}", 66 | ) 67 | 68 | 69 | EMPTY_CONTEXT = Context() 70 | -------------------------------------------------------------------------------- /src/anchorpy/program/namespace/__init__.py: -------------------------------------------------------------------------------- 1 | """This subpackage deals with the dynamic namespaces under `Program`.""" 2 | -------------------------------------------------------------------------------- /src/anchorpy/program/namespace/rpc.py: -------------------------------------------------------------------------------- 1 | """This module contains code for generating RPC functions.""" 2 | from typing import Any, Awaitable, Dict, Protocol 3 | 4 | from anchorpy_core.idl import IdlInstruction 5 | from solana.rpc.commitment import Confirmed 6 | from solana.rpc.core import RPCException 7 | from solders.pubkey import Pubkey 8 | from solders.signature import Signature 9 | 10 | from anchorpy.error import ProgramError 11 | from anchorpy.program.context import EMPTY_CONTEXT, Context, _check_args_length 12 | from anchorpy.program.namespace.transaction import _TransactionFn 13 | from anchorpy.provider import Provider 14 | 15 | 16 | class _RpcFn(Protocol): 17 | """_RpcFn is a single RPC method generated from an IDL, sending a transaction paid for and signed by the configured provider.""" # noqa: E501 18 | 19 | def __call__( 20 | self, 21 | *args: Any, 22 | ctx: Context = EMPTY_CONTEXT, 23 | ) -> Awaitable[Signature]: 24 | """Call the function (this is just a protocol declaration). 25 | 26 | Args: 27 | *args: The positional arguments for the program. The type and number 28 | of these arguments depend on the program being used. 29 | ctx: non-argument parameters to pass to the method. 30 | """ 31 | ... 32 | 33 | 34 | def _build_rpc_item( # ts: RpcFactory 35 | idl_ix: IdlInstruction, 36 | tx_fn: _TransactionFn, 37 | idl_errors: Dict[int, str], 38 | provider: Provider, 39 | program_id: Pubkey, 40 | ) -> _RpcFn: 41 | """Build the function that sends transactions for the given method. 42 | 43 | Args: 44 | idl_ix: The IDL instruction object. 45 | tx_fn: The function that generates the `Transaction` to send. 46 | idl_errors: Mapping of error code to error message. 47 | provider: Anchor Provider instance. 48 | program_id: The ID of the Anchor program. 49 | 50 | Returns: 51 | The RPC function. 52 | """ 53 | 54 | async def rpc_fn(*args: Any, ctx: Context = EMPTY_CONTEXT) -> Signature: 55 | recent_blockhash = ( 56 | await provider.connection.get_latest_blockhash(Confirmed) 57 | ).value.blockhash 58 | tx = tx_fn( 59 | *args, payer=provider.wallet.payer, blockhash=recent_blockhash, ctx=ctx 60 | ) 61 | _check_args_length(idl_ix, args) 62 | try: 63 | return await provider.send(tx, ctx.options) 64 | except RPCException as e: 65 | err_info = e.args[0] 66 | translated_err = ProgramError.parse(err_info, idl_errors, program_id) 67 | if translated_err is not None: 68 | raise translated_err from e 69 | raise 70 | 71 | return rpc_fn 72 | -------------------------------------------------------------------------------- /src/anchorpy/program/namespace/simulate.py: -------------------------------------------------------------------------------- 1 | """This module contains code for creating simulate functions.""" 2 | from typing import Any, Awaitable, Dict, NamedTuple, Protocol 3 | 4 | from anchorpy_core.idl import Idl, IdlInstruction 5 | from solana.rpc.commitment import Confirmed 6 | from solana.rpc.core import RPCException 7 | from solders.pubkey import Pubkey 8 | 9 | from anchorpy.coder.coder import Coder 10 | from anchorpy.error import ProgramError 11 | from anchorpy.program.context import EMPTY_CONTEXT, Context, _check_args_length 12 | from anchorpy.program.event import Event, EventParser 13 | from anchorpy.program.namespace.transaction import _TransactionFn 14 | from anchorpy.provider import Provider 15 | 16 | 17 | class SimulateResponse(NamedTuple): 18 | """The result of a simulate function call.""" 19 | 20 | events: list[Event] 21 | raw: list[str] 22 | 23 | 24 | class _SimulateFn(Protocol): 25 | """A single method generated from an IDL. 26 | 27 | It simulates a method against a cluster configured by the provider, 28 | returning a list of all the events and raw logs that were emitted 29 | during the execution of the method. 30 | """ 31 | 32 | def __call__( 33 | self, 34 | *args: Any, 35 | ctx: Context = EMPTY_CONTEXT, 36 | ) -> Awaitable[SimulateResponse]: 37 | """Protocol definition. 38 | 39 | Args: 40 | *args: The positional arguments for the program. The type and number 41 | of these arguments depend on the program being used. 42 | ctx: non-argument parameters to pass to the method. 43 | 44 | """ 45 | 46 | 47 | def _build_simulate_item( 48 | idl_ix: IdlInstruction, 49 | tx_fn: _TransactionFn, 50 | idl_errors: Dict[int, str], 51 | provider: Provider, 52 | coder: Coder, 53 | program_id: Pubkey, 54 | idl: Idl, 55 | ) -> _SimulateFn: 56 | """Build the function to simulate transactions for a given method of a program. 57 | 58 | Args: 59 | idl_ix: An IDL instruction object. 60 | tx_fn: The function to generate the `Transaction` object. 61 | idl_errors: Mapping of error code to message. 62 | provider: A provider instance. 63 | coder: The program's coder object. 64 | program_id: The program ID. 65 | idl: The parsed Idl instance. 66 | 67 | Returns: 68 | The simulate function. 69 | """ 70 | 71 | async def simulate_fn(*args: Any, ctx: Context = EMPTY_CONTEXT) -> SimulateResponse: 72 | blockhash = ( 73 | await provider.connection.get_latest_blockhash(Confirmed) 74 | ).value.blockhash 75 | tx = tx_fn(*args, payer=provider.wallet.payer, blockhash=blockhash, ctx=ctx) 76 | _check_args_length(idl_ix, args) 77 | resp = (await provider.simulate(tx, ctx.options)).value 78 | resp_err = resp.err 79 | logs = resp.logs or [] 80 | if resp_err is None: 81 | events = [] 82 | if idl.events is not None: 83 | parser = EventParser(program_id, coder) 84 | parser.parse_logs(logs, lambda evt: events.append(evt)) 85 | return SimulateResponse(events, logs) 86 | else: 87 | translated_err = ProgramError.parse_tx_error( 88 | resp_err, idl_errors, program_id, logs 89 | ) 90 | if translated_err is not None: 91 | raise translated_err 92 | raise RPCException(resp_err) 93 | 94 | return simulate_fn 95 | -------------------------------------------------------------------------------- /src/anchorpy/program/namespace/transaction.py: -------------------------------------------------------------------------------- 1 | """This module deals with generating transactions.""" 2 | from typing import Any, Protocol 3 | 4 | from anchorpy_core.idl import IdlInstruction 5 | from solders.hash import Hash 6 | from solders.instruction import Instruction 7 | from solders.keypair import Keypair 8 | from solders.message import Message 9 | from solders.transaction import VersionedTransaction 10 | 11 | from anchorpy.program.context import EMPTY_CONTEXT, Context, _check_args_length 12 | from anchorpy.program.namespace.instruction import _InstructionFn 13 | 14 | 15 | # ported from more-itertools 16 | def _unique_everseen(iterable): 17 | """Yield unique elements, preserving order. 18 | 19 | >>> list(_unique_everseen('AAAABBBCCDAABBB')) 20 | ['A', 'B', 'C', 'D'] 21 | """ 22 | seenset = set() 23 | seenset_add = seenset.add 24 | seenlist = [] 25 | seenlist_add = seenlist.append 26 | 27 | for element in iterable: 28 | try: 29 | if element not in seenset: 30 | seenset_add(element) 31 | yield element 32 | except TypeError: 33 | if element not in seenlist: 34 | seenlist_add(element) 35 | yield element 36 | 37 | 38 | class _TransactionFn(Protocol): 39 | """A function to create a `Transaction` for a given program instruction.""" 40 | 41 | def __call__( 42 | self, *args: Any, payer: Keypair, blockhash: Hash, ctx: Context = EMPTY_CONTEXT 43 | ) -> VersionedTransaction: 44 | """Make sure that the function looks like this. 45 | 46 | Args: 47 | *args: The positional arguments for the program. The type and number 48 | of these arguments depend on the program being used. 49 | payer: The transaction fee payer. 50 | blockhash: A recent blockhash. 51 | ctx: non-argument parameters to pass to the method. 52 | 53 | """ 54 | ... 55 | 56 | 57 | # ts TransactionNamespaceFactory.build 58 | def _build_transaction_fn( 59 | idl_ix: IdlInstruction, ix_fn: _InstructionFn 60 | ) -> _TransactionFn: 61 | """Build the function that generates Transaction objects. 62 | 63 | Args: 64 | idl_ix: Instruction item from the IDL. 65 | ix_fn (_InstructionFn): The function that generates instructions. 66 | 67 | Returns: 68 | _TransactionFn: [description] 69 | """ 70 | 71 | def tx_fn( 72 | *args: Any, payer: Keypair, blockhash: Hash, ctx: Context = EMPTY_CONTEXT 73 | ) -> VersionedTransaction: 74 | ixns: list[Instruction] = [] 75 | _check_args_length(idl_ix, args) 76 | if ctx.pre_instructions: 77 | ixns.extend(ctx.pre_instructions) 78 | ixns.append(ix_fn(*args, ctx=ctx)) 79 | if ctx.post_instructions: 80 | ixns.extend(ctx.post_instructions) 81 | ctx_signers = ctx.signers 82 | signers = [] if ctx_signers is None else ctx_signers 83 | all_signers = list(_unique_everseen([payer, *signers])) 84 | msg = Message.new_with_blockhash(ixns, payer.pubkey(), blockhash) 85 | return VersionedTransaction(msg, all_signers) 86 | 87 | return tx_fn 88 | -------------------------------------------------------------------------------- /src/anchorpy/program/namespace/types.py: -------------------------------------------------------------------------------- 1 | """This module contains code for handling user-defined types.""" 2 | from typing import Any, Type 3 | 4 | from anchorpy_core.idl import Idl 5 | 6 | from anchorpy.coder.idl import _idl_typedef_to_python_type 7 | 8 | 9 | def _build_types( 10 | idl: Idl, 11 | ) -> dict[str, Type[Any]]: 12 | """Generate the `.type` namespace. 13 | 14 | Args: 15 | idl: A parsed `Idl` instance. 16 | 17 | Returns: 18 | Mapping of type name to Python object. 19 | """ 20 | result = {} 21 | for idl_type in idl.types: 22 | try: 23 | python_type = _idl_typedef_to_python_type(idl_type, idl.types) 24 | except ValueError: 25 | continue 26 | result[idl_type.name] = python_type 27 | return result 28 | -------------------------------------------------------------------------------- /src/anchorpy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/src/anchorpy/py.typed -------------------------------------------------------------------------------- /src/anchorpy/template.py: -------------------------------------------------------------------------------- 1 | INIT_TESTS = '''"""AnchorPy integration tests.""" 2 | import asyncio 3 | from pytest import fixture, mark 4 | 5 | from anchorpy import Program 6 | from anchorpy.pytest_plugin import workspace_fixture 7 | from anchorpy.workspace import WorkspaceType 8 | 9 | 10 | @fixture(scope="module") 11 | def event_loop(): 12 | """Create a module-scoped event loop so we can use module-scope async fixtures.""" 13 | loop = asyncio.get_event_loop_policy().new_event_loop() # noqa: DAR301 14 | yield loop 15 | loop.close() 16 | 17 | 18 | workspace = workspace_fixture(".") 19 | 20 | 21 | @fixture(scope="module") 22 | def program(workspace: WorkspaceType) -> Program: 23 | """Create a Program instance.""" 24 | return workspace["{}"] 25 | 26 | 27 | @mark.asyncio 28 | async def test_init(program: Program) -> None: 29 | """Test that the initialize function is invoked successfully.""" 30 | await program.rpc["initialize"]() 31 | ''' 32 | -------------------------------------------------------------------------------- /src/anchorpy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Various utility functions.""" 2 | from anchorpy.utils import rpc, token 3 | 4 | __all__ = ["rpc", "token"] 5 | -------------------------------------------------------------------------------- /src/anchorpy/workspace.py: -------------------------------------------------------------------------------- 1 | """This module contains code for creating the Anchor workspace.""" 2 | from pathlib import Path 3 | from typing import Dict, Optional, Union 4 | 5 | import toml # type: ignore 6 | from anchorpy_core.idl import Idl 7 | from solders.pubkey import Pubkey 8 | 9 | from anchorpy.program.core import Program 10 | from anchorpy.provider import Provider 11 | 12 | WorkspaceType = Dict[str, Program] 13 | 14 | 15 | def create_workspace( 16 | path: Optional[Union[Path, str]] = None, url: Optional[str] = None 17 | ) -> WorkspaceType: 18 | """Get a workspace from the provided path to the project root. 19 | 20 | Args: 21 | path: The path to the project root. Defaults to the current working 22 | directory if omitted. 23 | url: The URL of the JSON RPC. Defaults to http://localhost:8899. 24 | 25 | Returns: 26 | Mapping of program name to Program object. 27 | """ 28 | result = {} 29 | project_root = Path.cwd() if path is None else Path(path) 30 | idl_folder = project_root / "target/idl" 31 | localnet_programs: dict[str, str] = toml.load(project_root / "Anchor.toml")[ 32 | "programs" 33 | ]["localnet"] 34 | for file in idl_folder.iterdir(): 35 | raw = file.read_text() 36 | idl = Idl.from_json(raw) 37 | name = idl.name 38 | program_id = Pubkey.from_string(localnet_programs[name]) 39 | program = Program(idl, program_id, Provider.local(url)) 40 | result[idl.name] = program 41 | return result 42 | 43 | 44 | async def close_workspace(workspace: WorkspaceType) -> None: 45 | """Close the HTTP clients of all the programs in the workspace. 46 | 47 | Args: 48 | workspace: The workspace to close. 49 | """ 50 | for program in workspace.values(): 51 | # could do this in a faster way but there's probably no point. 52 | await program.close() 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/tests/__init__.py -------------------------------------------------------------------------------- /tests/client_gen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/tests/client_gen/__init__.py -------------------------------------------------------------------------------- /tests/client_gen/example-program/Anchor.toml: -------------------------------------------------------------------------------- 1 | [programs.localnet] 2 | example_program = "3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8" 3 | 4 | [registry] 5 | url = "https://anchor.projectserum.com" 6 | 7 | [provider] 8 | cluster = "localnet" 9 | wallet = "~/.config/solana/id.json" 10 | 11 | [features] 12 | seeds = true 13 | -------------------------------------------------------------------------------- /tests/client_gen/example-program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | -------------------------------------------------------------------------------- /tests/client_gen/example-program/programs/example-program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-program" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "example_program" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.29.0" 20 | -------------------------------------------------------------------------------- /tests/client_gen/example-program/programs/example-program/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/tests/client_gen/example_program_gen/__init__.py -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | from .my_account import MyAccount, MyAccountJSON 2 | from .base_account import BaseAccount, BaseAccountJSON 3 | from .state import State, StateJSON 4 | from .state2 import State2, State2JSON 5 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/accounts/base_account.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | from solders.pubkey import Pubkey 4 | from solana.rpc.async_api import AsyncClient 5 | from solana.rpc.commitment import Commitment 6 | import borsh_construct as borsh 7 | from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE 8 | from anchorpy.error import AccountInvalidDiscriminator 9 | from anchorpy.utils.rpc import get_multiple_accounts 10 | from anchorpy.borsh_extension import BorshPubkey 11 | from ..program_id import PROGRAM_ID 12 | 13 | 14 | class BaseAccountJSON(typing.TypedDict): 15 | base_data: int 16 | base_data_key: str 17 | 18 | 19 | @dataclass 20 | class BaseAccount: 21 | discriminator: typing.ClassVar = b"\x10Z\x82\xf2\x9f\n\xe8\x85" 22 | layout: typing.ClassVar = borsh.CStruct( 23 | "base_data" / borsh.U64, "base_data_key" / BorshPubkey 24 | ) 25 | base_data: int 26 | base_data_key: Pubkey 27 | 28 | @classmethod 29 | async def fetch( 30 | cls, 31 | conn: AsyncClient, 32 | address: Pubkey, 33 | commitment: typing.Optional[Commitment] = None, 34 | program_id: Pubkey = PROGRAM_ID, 35 | ) -> typing.Optional["BaseAccount"]: 36 | resp = await conn.get_account_info(address, commitment=commitment) 37 | info = resp.value 38 | if info is None: 39 | return None 40 | if info.owner != program_id: 41 | raise ValueError("Account does not belong to this program") 42 | bytes_data = info.data 43 | return cls.decode(bytes_data) 44 | 45 | @classmethod 46 | async def fetch_multiple( 47 | cls, 48 | conn: AsyncClient, 49 | addresses: list[Pubkey], 50 | commitment: typing.Optional[Commitment] = None, 51 | program_id: Pubkey = PROGRAM_ID, 52 | ) -> typing.List[typing.Optional["BaseAccount"]]: 53 | infos = await get_multiple_accounts(conn, addresses, commitment=commitment) 54 | res: typing.List[typing.Optional["BaseAccount"]] = [] 55 | for info in infos: 56 | if info is None: 57 | res.append(None) 58 | continue 59 | if info.account.owner != program_id: 60 | raise ValueError("Account does not belong to this program") 61 | res.append(cls.decode(info.account.data)) 62 | return res 63 | 64 | @classmethod 65 | def decode(cls, data: bytes) -> "BaseAccount": 66 | if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator: 67 | raise AccountInvalidDiscriminator( 68 | "The discriminator for this account is invalid" 69 | ) 70 | dec = BaseAccount.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:]) 71 | return cls( 72 | base_data=dec.base_data, 73 | base_data_key=dec.base_data_key, 74 | ) 75 | 76 | def to_json(self) -> BaseAccountJSON: 77 | return { 78 | "base_data": self.base_data, 79 | "base_data_key": str(self.base_data_key), 80 | } 81 | 82 | @classmethod 83 | def from_json(cls, obj: BaseAccountJSON) -> "BaseAccount": 84 | return cls( 85 | base_data=obj["base_data"], 86 | base_data_key=Pubkey.from_string(obj["base_data_key"]), 87 | ) 88 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/accounts/my_account.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | from solders.pubkey import Pubkey 4 | from solana.rpc.async_api import AsyncClient 5 | from solana.rpc.commitment import Commitment 6 | import borsh_construct as borsh 7 | from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE 8 | from anchorpy.error import AccountInvalidDiscriminator 9 | from anchorpy.utils.rpc import get_multiple_accounts 10 | from ..program_id import PROGRAM_ID 11 | 12 | 13 | class MyAccountJSON(typing.TypedDict): 14 | data: int 15 | 16 | 17 | @dataclass 18 | class MyAccount: 19 | discriminator: typing.ClassVar = b"\xf6\x1c\x06W\xfb-2*" 20 | layout: typing.ClassVar = borsh.CStruct("data" / borsh.U64) 21 | data: int 22 | 23 | @classmethod 24 | async def fetch( 25 | cls, 26 | conn: AsyncClient, 27 | address: Pubkey, 28 | commitment: typing.Optional[Commitment] = None, 29 | program_id: Pubkey = PROGRAM_ID, 30 | ) -> typing.Optional["MyAccount"]: 31 | resp = await conn.get_account_info(address, commitment=commitment) 32 | info = resp.value 33 | if info is None: 34 | return None 35 | if info.owner != program_id: 36 | raise ValueError("Account does not belong to this program") 37 | bytes_data = info.data 38 | return cls.decode(bytes_data) 39 | 40 | @classmethod 41 | async def fetch_multiple( 42 | cls, 43 | conn: AsyncClient, 44 | addresses: list[Pubkey], 45 | commitment: typing.Optional[Commitment] = None, 46 | program_id: Pubkey = PROGRAM_ID, 47 | ) -> typing.List[typing.Optional["MyAccount"]]: 48 | infos = await get_multiple_accounts(conn, addresses, commitment=commitment) 49 | res: typing.List[typing.Optional["MyAccount"]] = [] 50 | for info in infos: 51 | if info is None: 52 | res.append(None) 53 | continue 54 | if info.account.owner != program_id: 55 | raise ValueError("Account does not belong to this program") 56 | res.append(cls.decode(info.account.data)) 57 | return res 58 | 59 | @classmethod 60 | def decode(cls, data: bytes) -> "MyAccount": 61 | if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator: 62 | raise AccountInvalidDiscriminator( 63 | "The discriminator for this account is invalid" 64 | ) 65 | dec = MyAccount.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:]) 66 | return cls( 67 | data=dec.data, 68 | ) 69 | 70 | def to_json(self) -> MyAccountJSON: 71 | return { 72 | "data": self.data, 73 | } 74 | 75 | @classmethod 76 | def from_json(cls, obj: MyAccountJSON) -> "MyAccount": 77 | return cls( 78 | data=obj["data"], 79 | ) 80 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/accounts/state2.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | from construct import Construct 4 | from solders.pubkey import Pubkey 5 | from solana.rpc.async_api import AsyncClient 6 | from solana.rpc.commitment import Commitment 7 | import borsh_construct as borsh 8 | from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE 9 | from anchorpy.error import AccountInvalidDiscriminator 10 | from anchorpy.utils.rpc import get_multiple_accounts 11 | from ..program_id import PROGRAM_ID 12 | 13 | 14 | class State2JSON(typing.TypedDict): 15 | vec_of_option: list[typing.Optional[int]] 16 | 17 | 18 | @dataclass 19 | class State2: 20 | discriminator: typing.ClassVar = b"ja\xff\xa1\xfa\xcd\xb9\xc0" 21 | layout: typing.ClassVar = borsh.CStruct( 22 | "vec_of_option" / borsh.Vec(typing.cast(Construct, borsh.Option(borsh.U64))) 23 | ) 24 | vec_of_option: list[typing.Optional[int]] 25 | 26 | @classmethod 27 | async def fetch( 28 | cls, 29 | conn: AsyncClient, 30 | address: Pubkey, 31 | commitment: typing.Optional[Commitment] = None, 32 | program_id: Pubkey = PROGRAM_ID, 33 | ) -> typing.Optional["State2"]: 34 | resp = await conn.get_account_info(address, commitment=commitment) 35 | info = resp.value 36 | if info is None: 37 | return None 38 | if info.owner != program_id: 39 | raise ValueError("Account does not belong to this program") 40 | bytes_data = info.data 41 | return cls.decode(bytes_data) 42 | 43 | @classmethod 44 | async def fetch_multiple( 45 | cls, 46 | conn: AsyncClient, 47 | addresses: list[Pubkey], 48 | commitment: typing.Optional[Commitment] = None, 49 | program_id: Pubkey = PROGRAM_ID, 50 | ) -> typing.List[typing.Optional["State2"]]: 51 | infos = await get_multiple_accounts(conn, addresses, commitment=commitment) 52 | res: typing.List[typing.Optional["State2"]] = [] 53 | for info in infos: 54 | if info is None: 55 | res.append(None) 56 | continue 57 | if info.account.owner != program_id: 58 | raise ValueError("Account does not belong to this program") 59 | res.append(cls.decode(info.account.data)) 60 | return res 61 | 62 | @classmethod 63 | def decode(cls, data: bytes) -> "State2": 64 | if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator: 65 | raise AccountInvalidDiscriminator( 66 | "The discriminator for this account is invalid" 67 | ) 68 | dec = State2.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:]) 69 | return cls( 70 | vec_of_option=dec.vec_of_option, 71 | ) 72 | 73 | def to_json(self) -> State2JSON: 74 | return { 75 | "vec_of_option": self.vec_of_option, 76 | } 77 | 78 | @classmethod 79 | def from_json(cls, obj: State2JSON) -> "State2": 80 | return cls( 81 | vec_of_option=obj["vec_of_option"], 82 | ) 83 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/errors/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import re 3 | from solders.transaction_status import ( 4 | InstructionErrorCustom, 5 | TransactionErrorInstructionError, 6 | ) 7 | from solana.rpc.core import RPCException 8 | from solders.rpc.errors import SendTransactionPreflightFailureMessage 9 | from anchorpy.error import extract_code_and_logs 10 | from ..program_id import PROGRAM_ID 11 | from . import anchor 12 | from . import custom 13 | 14 | 15 | def from_code(code: int) -> typing.Union[custom.CustomError, anchor.AnchorError, None]: 16 | return custom.from_code(code) if code >= 6000 else anchor.from_code(code) 17 | 18 | 19 | error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)") 20 | 21 | 22 | def from_tx_error( 23 | error: RPCException, 24 | ) -> typing.Union[anchor.AnchorError, custom.CustomError, None]: 25 | err_info = error.args[0] 26 | extracted = extract_code_and_logs(err_info, PROGRAM_ID) 27 | if extracted is None: 28 | return None 29 | return from_code(extracted[0]) 30 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/errors/custom.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from anchorpy.error import ProgramError 3 | 4 | 5 | class SomeError(ProgramError): 6 | def __init__(self) -> None: 7 | super().__init__(6000, "Example error.") 8 | 9 | code = 6000 10 | name = "SomeError" 11 | msg = "Example error." 12 | 13 | 14 | class OtherError(ProgramError): 15 | def __init__(self) -> None: 16 | super().__init__(6001, "Another error.") 17 | 18 | code = 6001 19 | name = "OtherError" 20 | msg = "Another error." 21 | 22 | 23 | CustomError = typing.Union[SomeError, OtherError] 24 | CUSTOM_ERROR_MAP: dict[int, CustomError] = { 25 | 6000: SomeError(), 26 | 6001: OtherError(), 27 | } 28 | 29 | 30 | def from_code(code: int) -> typing.Optional[CustomError]: 31 | maybe_err = CUSTOM_ERROR_MAP.get(code) 32 | if maybe_err is None: 33 | return None 34 | return maybe_err 35 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | from .initialize import initialize, InitializeAccounts 2 | from .initialize_with_values import ( 3 | initialize_with_values, 4 | InitializeWithValuesArgs, 5 | InitializeWithValuesAccounts, 6 | ) 7 | from .initialize_with_values2 import ( 8 | initialize_with_values2, 9 | InitializeWithValues2Args, 10 | InitializeWithValues2Accounts, 11 | ) 12 | from .cause_error import cause_error 13 | from .init_my_account import init_my_account, InitMyAccountArgs, InitMyAccountAccounts 14 | from .increment_state_when_present import ( 15 | increment_state_when_present, 16 | IncrementStateWhenPresentAccounts, 17 | ) 18 | from .type_alias import type_alias, TypeAliasArgs 19 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/cause_error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | def cause_error( 9 | program_id: Pubkey = PROGRAM_ID, 10 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 11 | ) -> Instruction: 12 | keys: list[AccountMeta] = [] 13 | if remaining_accounts is not None: 14 | keys += remaining_accounts 15 | identifier = b"Ch%\x11\x02\x9bD\x11" 16 | encoded_args = b"" 17 | data = identifier + encoded_args 18 | return Instruction(program_id, data, keys) 19 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/increment_state_when_present.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.system_program import ID as SYS_PROGRAM_ID 5 | from solders.instruction import Instruction, AccountMeta 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class IncrementStateWhenPresentAccounts(typing.TypedDict): 10 | first_state: typing.Optional[Pubkey] 11 | second_state: Pubkey 12 | 13 | 14 | def increment_state_when_present( 15 | accounts: IncrementStateWhenPresentAccounts, 16 | program_id: Pubkey = PROGRAM_ID, 17 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 18 | ) -> Instruction: 19 | keys: list[AccountMeta] = [ 20 | AccountMeta(pubkey=accounts["first_state"], is_signer=False, is_writable=True) 21 | if accounts["first_state"] 22 | else AccountMeta(pubkey=program_id, is_signer=False, is_writable=False), 23 | AccountMeta( 24 | pubkey=accounts["second_state"], is_signer=False, is_writable=False 25 | ), 26 | AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), 27 | ] 28 | if remaining_accounts is not None: 29 | keys += remaining_accounts 30 | identifier = b"\xf1!\xdd(\xa3Jy%" 31 | encoded_args = b"" 32 | data = identifier + encoded_args 33 | return Instruction(program_id, data, keys) 34 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/init_my_account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.system_program import ID as SYS_PROGRAM_ID 5 | from solders.instruction import Instruction, AccountMeta 6 | import borsh_construct as borsh 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class InitMyAccountArgs(typing.TypedDict): 11 | seed_a: int 12 | 13 | 14 | layout = borsh.CStruct("seed_a" / borsh.U8) 15 | NESTED_NESTED_ACCOUNT_NESTED = Pubkey.find_program_address( 16 | seeds=[ 17 | b"nested-seed", 18 | b"test", 19 | b"hi", 20 | b"hi", 21 | b"\x01", 22 | b"\x02\x00\x00\x00", 23 | b"\x03\x00\x00\x00\x00\x00\x00\x00", 24 | ], 25 | program_id=PROGRAM_ID, 26 | )[0] 27 | INIT_MY_ACCOUNT_ACCOUNTS_ACCOUNT = Pubkey.find_program_address( 28 | seeds=[ 29 | b"another-seed", 30 | b"test", 31 | b"hi", 32 | b"hi", 33 | b"\x01", 34 | b"\x02\x00\x00\x00", 35 | b"\x03\x00\x00\x00\x00\x00\x00\x00", 36 | ], 37 | program_id=PROGRAM_ID, 38 | )[0] 39 | 40 | 41 | class InitMyAccountAccounts(typing.TypedDict): 42 | base: Pubkey 43 | base2: Pubkey 44 | 45 | 46 | def init_my_account( 47 | args: InitMyAccountArgs, 48 | accounts: InitMyAccountAccounts, 49 | program_id: Pubkey = PROGRAM_ID, 50 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 51 | ) -> Instruction: 52 | keys: list[AccountMeta] = [ 53 | AccountMeta(pubkey=accounts["base"], is_signer=False, is_writable=False), 54 | AccountMeta(pubkey=accounts["base2"], is_signer=False, is_writable=False), 55 | AccountMeta( 56 | pubkey=INIT_MY_ACCOUNT_ACCOUNTS_ACCOUNT, is_signer=False, is_writable=False 57 | ), 58 | AccountMeta( 59 | pubkey=NESTED_NESTED_ACCOUNT_NESTED, is_signer=False, is_writable=False 60 | ), 61 | AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), 62 | ] 63 | if remaining_accounts is not None: 64 | keys += remaining_accounts 65 | identifier = b" ;\x05\xcd^E\xe3z" 66 | encoded_args = layout.build( 67 | { 68 | "seed_a": args["seed_a"], 69 | } 70 | ) 71 | data = identifier + encoded_args 72 | return Instruction(program_id, data, keys) 73 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/initialize.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.system_program import ID as SYS_PROGRAM_ID 5 | from solders.sysvar import RENT, CLOCK 6 | from solders.instruction import Instruction, AccountMeta 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class InitializeAccounts(typing.TypedDict): 11 | state: Pubkey 12 | payer: Pubkey 13 | 14 | 15 | def initialize( 16 | accounts: InitializeAccounts, 17 | program_id: Pubkey = PROGRAM_ID, 18 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 19 | ) -> Instruction: 20 | keys: list[AccountMeta] = [ 21 | AccountMeta(pubkey=accounts["state"], is_signer=True, is_writable=True), 22 | AccountMeta(pubkey=CLOCK, is_signer=False, is_writable=False), 23 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 24 | AccountMeta(pubkey=accounts["payer"], is_signer=True, is_writable=True), 25 | AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), 26 | ] 27 | if remaining_accounts is not None: 28 | keys += remaining_accounts 29 | identifier = b"\xaf\xafm\x1f\r\x98\x9b\xed" 30 | encoded_args = b"" 31 | data = identifier + encoded_args 32 | return Instruction(program_id, data, keys) 33 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/initialize_with_values2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.system_program import ID as SYS_PROGRAM_ID 5 | from solders.instruction import Instruction, AccountMeta 6 | from construct import Construct 7 | import borsh_construct as borsh 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class InitializeWithValues2Args(typing.TypedDict): 12 | vec_of_option: list[typing.Optional[int]] 13 | 14 | 15 | layout = borsh.CStruct( 16 | "vec_of_option" / borsh.Vec(typing.cast(Construct, borsh.Option(borsh.U64))) 17 | ) 18 | 19 | 20 | class InitializeWithValues2Accounts(typing.TypedDict): 21 | state: Pubkey 22 | payer: Pubkey 23 | 24 | 25 | def initialize_with_values2( 26 | args: InitializeWithValues2Args, 27 | accounts: InitializeWithValues2Accounts, 28 | program_id: Pubkey = PROGRAM_ID, 29 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 30 | ) -> Instruction: 31 | keys: list[AccountMeta] = [ 32 | AccountMeta(pubkey=accounts["state"], is_signer=True, is_writable=True), 33 | AccountMeta(pubkey=accounts["payer"], is_signer=True, is_writable=True), 34 | AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), 35 | ] 36 | if remaining_accounts is not None: 37 | keys += remaining_accounts 38 | identifier = b"\xf8\xbe\x15a\xef\x94'\xb5" 39 | encoded_args = layout.build( 40 | { 41 | "vec_of_option": args["vec_of_option"], 42 | } 43 | ) 44 | data = identifier + encoded_args 45 | return Instruction(program_id, data, keys) 46 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/instructions/type_alias.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from .. import types 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class TypeAliasArgs(typing.TypedDict): 11 | u8_array: types.u8_array.U8Array 12 | 13 | 14 | layout = borsh.CStruct("u8_array" / borsh.U8[8]) 15 | 16 | 17 | def type_alias( 18 | args: TypeAliasArgs, 19 | program_id: Pubkey = PROGRAM_ID, 20 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 21 | ) -> Instruction: 22 | keys: list[AccountMeta] = [] 23 | if remaining_accounts is not None: 24 | keys += remaining_accounts 25 | identifier = b"5\x84\x11\xf9y\xc216" 26 | encoded_args = layout.build( 27 | { 28 | "u8_array": args["u8_array"], 29 | } 30 | ) 31 | data = identifier + encoded_args 32 | return Instruction(program_id, data, keys) 33 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/program_id.py: -------------------------------------------------------------------------------- 1 | from solders.pubkey import Pubkey 2 | 3 | PROGRAM_ID = Pubkey.from_string("3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8") 4 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/types/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from . import bar_struct 3 | from .bar_struct import BarStruct, BarStructJSON 4 | from . import foo_struct 5 | from .foo_struct import FooStruct, FooStructJSON 6 | from . import foo_enum 7 | from .foo_enum import FooEnumKind, FooEnumJSON 8 | from . import u8_array 9 | from .u8_array import U8Array 10 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/types/bar_struct.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from construct import Container 5 | import borsh_construct as borsh 6 | 7 | 8 | class BarStructJSON(typing.TypedDict): 9 | some_field: bool 10 | other_field: int 11 | 12 | 13 | @dataclass 14 | class BarStruct: 15 | layout: typing.ClassVar = borsh.CStruct( 16 | "some_field" / borsh.Bool, "other_field" / borsh.U8 17 | ) 18 | some_field: bool 19 | other_field: int 20 | 21 | @classmethod 22 | def from_decoded(cls, obj: Container) -> "BarStruct": 23 | return cls(some_field=obj.some_field, other_field=obj.other_field) 24 | 25 | def to_encodable(self) -> dict[str, typing.Any]: 26 | return {"some_field": self.some_field, "other_field": self.other_field} 27 | 28 | def to_json(self) -> BarStructJSON: 29 | return {"some_field": self.some_field, "other_field": self.other_field} 30 | 31 | @classmethod 32 | def from_json(cls, obj: BarStructJSON) -> "BarStruct": 33 | return cls(some_field=obj["some_field"], other_field=obj["other_field"]) 34 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/types/foo_struct.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from . import bar_struct, foo_enum 3 | import typing 4 | from dataclasses import dataclass 5 | from construct import Container, Construct 6 | import borsh_construct as borsh 7 | 8 | 9 | class FooStructJSON(typing.TypedDict): 10 | field1: int 11 | field2: int 12 | nested: bar_struct.BarStructJSON 13 | vec_nested: list[bar_struct.BarStructJSON] 14 | option_nested: typing.Optional[bar_struct.BarStructJSON] 15 | enum_field: foo_enum.FooEnumJSON 16 | 17 | 18 | @dataclass 19 | class FooStruct: 20 | layout: typing.ClassVar = borsh.CStruct( 21 | "field1" / borsh.U8, 22 | "field2" / borsh.U16, 23 | "nested" / bar_struct.BarStruct.layout, 24 | "vec_nested" / borsh.Vec(typing.cast(Construct, bar_struct.BarStruct.layout)), 25 | "option_nested" / borsh.Option(bar_struct.BarStruct.layout), 26 | "enum_field" / foo_enum.layout, 27 | ) 28 | field1: int 29 | field2: int 30 | nested: bar_struct.BarStruct 31 | vec_nested: list[bar_struct.BarStruct] 32 | option_nested: typing.Optional[bar_struct.BarStruct] 33 | enum_field: foo_enum.FooEnumKind 34 | 35 | @classmethod 36 | def from_decoded(cls, obj: Container) -> "FooStruct": 37 | return cls( 38 | field1=obj.field1, 39 | field2=obj.field2, 40 | nested=bar_struct.BarStruct.from_decoded(obj.nested), 41 | vec_nested=list( 42 | map( 43 | lambda item: bar_struct.BarStruct.from_decoded(item), obj.vec_nested 44 | ) 45 | ), 46 | option_nested=( 47 | None 48 | if obj.option_nested is None 49 | else bar_struct.BarStruct.from_decoded(obj.option_nested) 50 | ), 51 | enum_field=foo_enum.from_decoded(obj.enum_field), 52 | ) 53 | 54 | def to_encodable(self) -> dict[str, typing.Any]: 55 | return { 56 | "field1": self.field1, 57 | "field2": self.field2, 58 | "nested": self.nested.to_encodable(), 59 | "vec_nested": list(map(lambda item: item.to_encodable(), self.vec_nested)), 60 | "option_nested": ( 61 | None 62 | if self.option_nested is None 63 | else self.option_nested.to_encodable() 64 | ), 65 | "enum_field": self.enum_field.to_encodable(), 66 | } 67 | 68 | def to_json(self) -> FooStructJSON: 69 | return { 70 | "field1": self.field1, 71 | "field2": self.field2, 72 | "nested": self.nested.to_json(), 73 | "vec_nested": list(map(lambda item: item.to_json(), self.vec_nested)), 74 | "option_nested": ( 75 | None if self.option_nested is None else self.option_nested.to_json() 76 | ), 77 | "enum_field": self.enum_field.to_json(), 78 | } 79 | 80 | @classmethod 81 | def from_json(cls, obj: FooStructJSON) -> "FooStruct": 82 | return cls( 83 | field1=obj["field1"], 84 | field2=obj["field2"], 85 | nested=bar_struct.BarStruct.from_json(obj["nested"]), 86 | vec_nested=list( 87 | map( 88 | lambda item: bar_struct.BarStruct.from_json(item), obj["vec_nested"] 89 | ) 90 | ), 91 | option_nested=( 92 | None 93 | if obj["option_nested"] is None 94 | else bar_struct.BarStruct.from_json(obj["option_nested"]) 95 | ), 96 | enum_field=foo_enum.from_json(obj["enum_field"]), 97 | ) 98 | -------------------------------------------------------------------------------- /tests/client_gen/example_program_gen/types/u8_array.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | U8Array = list[int] 4 | -------------------------------------------------------------------------------- /tests/client_gen/token/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinheavey/anchorpy/8d36e883421fb6638c57d845b264d3aa599e97b2/tests/client_gen/token/__init__.py -------------------------------------------------------------------------------- /tests/client_gen/token/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | from .mint import Mint, MintJSON 2 | from .account import Account, AccountJSON 3 | from .multisig import Multisig, MultisigJSON 4 | -------------------------------------------------------------------------------- /tests/client_gen/token/accounts/multisig.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | from solders.pubkey import Pubkey 4 | from solana.rpc.async_api import AsyncClient 5 | from solana.rpc.commitment import Commitment 6 | import borsh_construct as borsh 7 | from anchorpy.coder.accounts import ACCOUNT_DISCRIMINATOR_SIZE 8 | from anchorpy.error import AccountInvalidDiscriminator 9 | from anchorpy.utils.rpc import get_multiple_accounts 10 | from anchorpy.borsh_extension import BorshPubkey 11 | from ..program_id import PROGRAM_ID 12 | 13 | 14 | class MultisigJSON(typing.TypedDict): 15 | m: int 16 | n: int 17 | is_initialized: bool 18 | signers: list[str] 19 | 20 | 21 | @dataclass 22 | class Multisig: 23 | discriminator: typing.ClassVar = b"\xe0ty\xbaD\xa1O\xec" 24 | layout: typing.ClassVar = borsh.CStruct( 25 | "m" / borsh.U8, 26 | "n" / borsh.U8, 27 | "is_initialized" / borsh.Bool, 28 | "signers" / BorshPubkey[11], 29 | ) 30 | m: int 31 | n: int 32 | is_initialized: bool 33 | signers: list[Pubkey] 34 | 35 | @classmethod 36 | async def fetch( 37 | cls, 38 | conn: AsyncClient, 39 | address: Pubkey, 40 | commitment: typing.Optional[Commitment] = None, 41 | program_id: Pubkey = PROGRAM_ID, 42 | ) -> typing.Optional["Multisig"]: 43 | resp = await conn.get_account_info(address, commitment=commitment) 44 | info = resp.value 45 | if info is None: 46 | return None 47 | if info.owner != program_id: 48 | raise ValueError("Account does not belong to this program") 49 | bytes_data = info.data 50 | return cls.decode(bytes_data) 51 | 52 | @classmethod 53 | async def fetch_multiple( 54 | cls, 55 | conn: AsyncClient, 56 | addresses: list[Pubkey], 57 | commitment: typing.Optional[Commitment] = None, 58 | program_id: Pubkey = PROGRAM_ID, 59 | ) -> typing.List[typing.Optional["Multisig"]]: 60 | infos = await get_multiple_accounts(conn, addresses, commitment=commitment) 61 | res: typing.List[typing.Optional["Multisig"]] = [] 62 | for info in infos: 63 | if info is None: 64 | res.append(None) 65 | continue 66 | if info.account.owner != program_id: 67 | raise ValueError("Account does not belong to this program") 68 | res.append(cls.decode(info.account.data)) 69 | return res 70 | 71 | @classmethod 72 | def decode(cls, data: bytes) -> "Multisig": 73 | if data[:ACCOUNT_DISCRIMINATOR_SIZE] != cls.discriminator: 74 | raise AccountInvalidDiscriminator( 75 | "The discriminator for this account is invalid" 76 | ) 77 | dec = Multisig.layout.parse(data[ACCOUNT_DISCRIMINATOR_SIZE:]) 78 | return cls( 79 | m=dec.m, 80 | n=dec.n, 81 | is_initialized=dec.is_initialized, 82 | signers=dec.signers, 83 | ) 84 | 85 | def to_json(self) -> MultisigJSON: 86 | return { 87 | "m": self.m, 88 | "n": self.n, 89 | "is_initialized": self.is_initialized, 90 | "signers": list(map(lambda item: str(item), self.signers)), 91 | } 92 | 93 | @classmethod 94 | def from_json(cls, obj: MultisigJSON) -> "Multisig": 95 | return cls( 96 | m=obj["m"], 97 | n=obj["n"], 98 | is_initialized=obj["is_initialized"], 99 | signers=list(map(lambda item: Pubkey.from_string(item), obj["signers"])), 100 | ) 101 | -------------------------------------------------------------------------------- /tests/client_gen/token/errors/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import re 3 | from solders.transaction_status import ( 4 | InstructionErrorCustom, 5 | TransactionErrorInstructionError, 6 | ) 7 | from solana.rpc.core import RPCException 8 | from solders.rpc.errors import SendTransactionPreflightFailureMessage 9 | from anchorpy.error import extract_code_and_logs 10 | from ..program_id import PROGRAM_ID 11 | from . import anchor 12 | from . import custom 13 | 14 | 15 | def from_code(code: int) -> typing.Union[custom.CustomError, anchor.AnchorError, None]: 16 | return custom.from_code(code) if code >= 6000 else anchor.from_code(code) 17 | 18 | 19 | error_re = re.compile(r"Program (\w+) failed: custom program error: (\w+)") 20 | 21 | 22 | def from_tx_error( 23 | error: RPCException, 24 | ) -> typing.Union[anchor.AnchorError, custom.CustomError, None]: 25 | err_info = error.args[0] 26 | extracted = extract_code_and_logs(err_info, PROGRAM_ID) 27 | if extracted is None: 28 | return None 29 | return from_code(extracted[0]) 30 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | from .initialize_mint import initialize_mint, InitializeMintArgs, InitializeMintAccounts 2 | from .initialize_account import initialize_account, InitializeAccountAccounts 3 | from .initialize_multisig import ( 4 | initialize_multisig, 5 | InitializeMultisigArgs, 6 | InitializeMultisigAccounts, 7 | ) 8 | from .transfer import transfer, TransferArgs, TransferAccounts 9 | from .approve import approve, ApproveArgs, ApproveAccounts 10 | from .revoke import revoke, RevokeAccounts 11 | from .set_authority import set_authority, SetAuthorityArgs, SetAuthorityAccounts 12 | from .mint_to import mint_to, MintToArgs, MintToAccounts 13 | from .burn import burn, BurnArgs, BurnAccounts 14 | from .close_account import close_account, CloseAccountAccounts 15 | from .freeze_account import freeze_account, FreezeAccountAccounts 16 | from .thaw_account import thaw_account, ThawAccountAccounts 17 | from .transfer_checked import ( 18 | transfer_checked, 19 | TransferCheckedArgs, 20 | TransferCheckedAccounts, 21 | ) 22 | from .approve_checked import approve_checked, ApproveCheckedArgs, ApproveCheckedAccounts 23 | from .mint_to_checked import mint_to_checked, MintToCheckedArgs, MintToCheckedAccounts 24 | from .burn_checked import burn_checked, BurnCheckedArgs, BurnCheckedAccounts 25 | from .initialize_account2 import ( 26 | initialize_account2, 27 | InitializeAccount2Args, 28 | InitializeAccount2Accounts, 29 | ) 30 | from .sync_native import sync_native, SyncNativeAccounts 31 | from .initialize_account3 import ( 32 | initialize_account3, 33 | InitializeAccount3Args, 34 | InitializeAccount3Accounts, 35 | ) 36 | from .initialize_multisig2 import ( 37 | initialize_multisig2, 38 | InitializeMultisig2Args, 39 | InitializeMultisig2Accounts, 40 | ) 41 | from .initialize_mint2 import ( 42 | initialize_mint2, 43 | InitializeMint2Args, 44 | InitializeMint2Accounts, 45 | ) 46 | from .get_account_data_size import get_account_data_size, GetAccountDataSizeAccounts 47 | from .initialize_immutable_owner import ( 48 | initialize_immutable_owner, 49 | InitializeImmutableOwnerAccounts, 50 | ) 51 | from .amount_to_ui_amount import ( 52 | amount_to_ui_amount, 53 | AmountToUiAmountArgs, 54 | AmountToUiAmountAccounts, 55 | ) 56 | from .ui_amount_to_amount import ( 57 | ui_amount_to_amount, 58 | UiAmountToAmountArgs, 59 | UiAmountToAmountAccounts, 60 | ) 61 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/amount_to_ui_amount.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class AmountToUiAmountArgs(typing.TypedDict): 10 | amount: int 11 | 12 | 13 | layout = borsh.CStruct("amount" / borsh.U64) 14 | 15 | 16 | class AmountToUiAmountAccounts(typing.TypedDict): 17 | mint: Pubkey 18 | 19 | 20 | def amount_to_ui_amount( 21 | args: AmountToUiAmountArgs, 22 | accounts: AmountToUiAmountAccounts, 23 | program_id: Pubkey = PROGRAM_ID, 24 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 25 | ) -> Instruction: 26 | keys: list[AccountMeta] = [ 27 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False) 28 | ] 29 | if remaining_accounts is not None: 30 | keys += remaining_accounts 31 | identifier = b"\xa0\x91\xc8b\xf2\x9c\x1eZ" 32 | encoded_args = layout.build( 33 | { 34 | "amount": args["amount"], 35 | } 36 | ) 37 | data = identifier + encoded_args 38 | return Instruction(program_id, data, keys) 39 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/approve.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class ApproveArgs(typing.TypedDict): 10 | amount: int 11 | 12 | 13 | layout = borsh.CStruct("amount" / borsh.U64) 14 | 15 | 16 | class ApproveAccounts(typing.TypedDict): 17 | source: Pubkey 18 | delegate: Pubkey 19 | owner: Pubkey 20 | 21 | 22 | def approve( 23 | args: ApproveArgs, 24 | accounts: ApproveAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["source"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["delegate"], is_signer=False, is_writable=False), 31 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 32 | ] 33 | if remaining_accounts is not None: 34 | keys += remaining_accounts 35 | identifier = b"EJ\xd9$suaL" 36 | encoded_args = layout.build( 37 | { 38 | "amount": args["amount"], 39 | } 40 | ) 41 | data = identifier + encoded_args 42 | return Instruction(program_id, data, keys) 43 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/approve_checked.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class ApproveCheckedArgs(typing.TypedDict): 10 | amount: int 11 | decimals: int 12 | 13 | 14 | layout = borsh.CStruct("amount" / borsh.U64, "decimals" / borsh.U8) 15 | 16 | 17 | class ApproveCheckedAccounts(typing.TypedDict): 18 | source: Pubkey 19 | mint: Pubkey 20 | delegate: Pubkey 21 | owner: Pubkey 22 | 23 | 24 | def approve_checked( 25 | args: ApproveCheckedArgs, 26 | accounts: ApproveCheckedAccounts, 27 | program_id: Pubkey = PROGRAM_ID, 28 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 29 | ) -> Instruction: 30 | keys: list[AccountMeta] = [ 31 | AccountMeta(pubkey=accounts["source"], is_signer=False, is_writable=True), 32 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 33 | AccountMeta(pubkey=accounts["delegate"], is_signer=False, is_writable=False), 34 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 35 | ] 36 | if remaining_accounts is not None: 37 | keys += remaining_accounts 38 | identifier = b"/\xc5\xfe*:\xc9:m" 39 | encoded_args = layout.build( 40 | { 41 | "amount": args["amount"], 42 | "decimals": args["decimals"], 43 | } 44 | ) 45 | data = identifier + encoded_args 46 | return Instruction(program_id, data, keys) 47 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/burn.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class BurnArgs(typing.TypedDict): 10 | amount: int 11 | 12 | 13 | layout = borsh.CStruct("amount" / borsh.U64) 14 | 15 | 16 | class BurnAccounts(typing.TypedDict): 17 | account: Pubkey 18 | mint: Pubkey 19 | authority: Pubkey 20 | 21 | 22 | def burn( 23 | args: BurnArgs, 24 | accounts: BurnAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["authority"], is_signer=True, is_writable=False), 32 | ] 33 | if remaining_accounts is not None: 34 | keys += remaining_accounts 35 | identifier = b"tn\x1d8k\xdb*]" 36 | encoded_args = layout.build( 37 | { 38 | "amount": args["amount"], 39 | } 40 | ) 41 | data = identifier + encoded_args 42 | return Instruction(program_id, data, keys) 43 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/burn_checked.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class BurnCheckedArgs(typing.TypedDict): 10 | amount: int 11 | decimals: int 12 | 13 | 14 | layout = borsh.CStruct("amount" / borsh.U64, "decimals" / borsh.U8) 15 | 16 | 17 | class BurnCheckedAccounts(typing.TypedDict): 18 | account: Pubkey 19 | mint: Pubkey 20 | authority: Pubkey 21 | 22 | 23 | def burn_checked( 24 | args: BurnCheckedArgs, 25 | accounts: BurnCheckedAccounts, 26 | program_id: Pubkey = PROGRAM_ID, 27 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 28 | ) -> Instruction: 29 | keys: list[AccountMeta] = [ 30 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True), 32 | AccountMeta(pubkey=accounts["authority"], is_signer=True, is_writable=False), 33 | ] 34 | if remaining_accounts is not None: 35 | keys += remaining_accounts 36 | identifier = b"\xc6y\xc8fx\xd0\x9b\xb2" 37 | encoded_args = layout.build( 38 | { 39 | "amount": args["amount"], 40 | "decimals": args["decimals"], 41 | } 42 | ) 43 | data = identifier + encoded_args 44 | return Instruction(program_id, data, keys) 45 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/close_account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class CloseAccountAccounts(typing.TypedDict): 9 | account: Pubkey 10 | destination: Pubkey 11 | owner: Pubkey 12 | 13 | 14 | def close_account( 15 | accounts: CloseAccountAccounts, 16 | program_id: Pubkey = PROGRAM_ID, 17 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 18 | ) -> Instruction: 19 | keys: list[AccountMeta] = [ 20 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 21 | AccountMeta(pubkey=accounts["destination"], is_signer=False, is_writable=True), 22 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 23 | ] 24 | if remaining_accounts is not None: 25 | keys += remaining_accounts 26 | identifier = b'}\xff\x95\x0en"H\x18' 27 | encoded_args = b"" 28 | data = identifier + encoded_args 29 | return Instruction(program_id, data, keys) 30 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/freeze_account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class FreezeAccountAccounts(typing.TypedDict): 9 | account: Pubkey 10 | mint: Pubkey 11 | owner: Pubkey 12 | 13 | 14 | def freeze_account( 15 | accounts: FreezeAccountAccounts, 16 | program_id: Pubkey = PROGRAM_ID, 17 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 18 | ) -> Instruction: 19 | keys: list[AccountMeta] = [ 20 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 21 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 22 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 23 | ] 24 | if remaining_accounts is not None: 25 | keys += remaining_accounts 26 | identifier = b"\xfdKR\x85\xa7\xee+\x82" 27 | encoded_args = b"" 28 | data = identifier + encoded_args 29 | return Instruction(program_id, data, keys) 30 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/get_account_data_size.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class GetAccountDataSizeAccounts(typing.TypedDict): 9 | mint: Pubkey 10 | 11 | 12 | def get_account_data_size( 13 | accounts: GetAccountDataSizeAccounts, 14 | program_id: Pubkey = PROGRAM_ID, 15 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 16 | ) -> Instruction: 17 | keys: list[AccountMeta] = [ 18 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False) 19 | ] 20 | if remaining_accounts is not None: 21 | keys += remaining_accounts 22 | identifier = b"\x10\xb1\xd2\x80\x15-o\x1f" 23 | encoded_args = b"" 24 | data = identifier + encoded_args 25 | return Instruction(program_id, data, keys) 26 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.sysvar import RENT 5 | from solders.instruction import Instruction, AccountMeta 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class InitializeAccountAccounts(typing.TypedDict): 10 | account: Pubkey 11 | mint: Pubkey 12 | owner: Pubkey 13 | 14 | 15 | def initialize_account( 16 | accounts: InitializeAccountAccounts, 17 | program_id: Pubkey = PROGRAM_ID, 18 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 19 | ) -> Instruction: 20 | keys: list[AccountMeta] = [ 21 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 22 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 23 | AccountMeta(pubkey=accounts["owner"], is_signer=False, is_writable=False), 24 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 25 | ] 26 | if remaining_accounts is not None: 27 | keys += remaining_accounts 28 | identifier = b"Jsc]\xc5Eg\x07" 29 | encoded_args = b"" 30 | data = identifier + encoded_args 31 | return Instruction(program_id, data, keys) 32 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_account2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.sysvar import RENT 5 | from solders.instruction import Instruction, AccountMeta 6 | from anchorpy.borsh_extension import BorshPubkey 7 | import borsh_construct as borsh 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class InitializeAccount2Args(typing.TypedDict): 12 | owner: Pubkey 13 | 14 | 15 | layout = borsh.CStruct("owner" / BorshPubkey) 16 | 17 | 18 | class InitializeAccount2Accounts(typing.TypedDict): 19 | account: Pubkey 20 | mint: Pubkey 21 | 22 | 23 | def initialize_account2( 24 | args: InitializeAccount2Args, 25 | accounts: InitializeAccount2Accounts, 26 | program_id: Pubkey = PROGRAM_ID, 27 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 28 | ) -> Instruction: 29 | keys: list[AccountMeta] = [ 30 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 32 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 33 | ] 34 | if remaining_accounts is not None: 35 | keys += remaining_accounts 36 | identifier = b"\x08\xb6\x95\x90\xb9\x1f\xd1i" 37 | encoded_args = layout.build( 38 | { 39 | "owner": args["owner"], 40 | } 41 | ) 42 | data = identifier + encoded_args 43 | return Instruction(program_id, data, keys) 44 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_account3.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from anchorpy.borsh_extension import BorshPubkey 6 | import borsh_construct as borsh 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class InitializeAccount3Args(typing.TypedDict): 11 | owner: Pubkey 12 | 13 | 14 | layout = borsh.CStruct("owner" / BorshPubkey) 15 | 16 | 17 | class InitializeAccount3Accounts(typing.TypedDict): 18 | account: Pubkey 19 | mint: Pubkey 20 | 21 | 22 | def initialize_account3( 23 | args: InitializeAccount3Args, 24 | accounts: InitializeAccount3Accounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 31 | ] 32 | if remaining_accounts is not None: 33 | keys += remaining_accounts 34 | identifier = b"\x17\x8e\x8c\x87\x15\xa0\x85@" 35 | encoded_args = layout.build( 36 | { 37 | "owner": args["owner"], 38 | } 39 | ) 40 | data = identifier + encoded_args 41 | return Instruction(program_id, data, keys) 42 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_immutable_owner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class InitializeImmutableOwnerAccounts(typing.TypedDict): 9 | account: Pubkey 10 | 11 | 12 | def initialize_immutable_owner( 13 | accounts: InitializeImmutableOwnerAccounts, 14 | program_id: Pubkey = PROGRAM_ID, 15 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 16 | ) -> Instruction: 17 | keys: list[AccountMeta] = [ 18 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True) 19 | ] 20 | if remaining_accounts is not None: 21 | keys += remaining_accounts 22 | identifier = b'\x8d2\x0f,\xc3\xf7"<' 23 | encoded_args = b"" 24 | data = identifier + encoded_args 25 | return Instruction(program_id, data, keys) 26 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_mint.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.sysvar import RENT 5 | from solders.instruction import Instruction, AccountMeta 6 | from anchorpy.borsh_extension import BorshPubkey, COption 7 | import borsh_construct as borsh 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class InitializeMintArgs(typing.TypedDict): 12 | decimals: int 13 | mint_authority: Pubkey 14 | freeze_authority: typing.Optional[Pubkey] 15 | 16 | 17 | layout = borsh.CStruct( 18 | "decimals" / borsh.U8, 19 | "mint_authority" / BorshPubkey, 20 | "freeze_authority" / COption(BorshPubkey), 21 | ) 22 | 23 | 24 | class InitializeMintAccounts(typing.TypedDict): 25 | mint: Pubkey 26 | 27 | 28 | def initialize_mint( 29 | args: InitializeMintArgs, 30 | accounts: InitializeMintAccounts, 31 | program_id: Pubkey = PROGRAM_ID, 32 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 33 | ) -> Instruction: 34 | keys: list[AccountMeta] = [ 35 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True), 36 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 37 | ] 38 | if remaining_accounts is not None: 39 | keys += remaining_accounts 40 | identifier = b"\xd1*\xc3\x04\x81U\xd1," 41 | encoded_args = layout.build( 42 | { 43 | "decimals": args["decimals"], 44 | "mint_authority": args["mint_authority"], 45 | "freeze_authority": args["freeze_authority"], 46 | } 47 | ) 48 | data = identifier + encoded_args 49 | return Instruction(program_id, data, keys) 50 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_mint2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from anchorpy.borsh_extension import BorshPubkey, COption 6 | import borsh_construct as borsh 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class InitializeMint2Args(typing.TypedDict): 11 | decimals: int 12 | mint_authority: Pubkey 13 | freeze_authority: typing.Optional[Pubkey] 14 | 15 | 16 | layout = borsh.CStruct( 17 | "decimals" / borsh.U8, 18 | "mint_authority" / BorshPubkey, 19 | "freeze_authority" / COption(BorshPubkey), 20 | ) 21 | 22 | 23 | class InitializeMint2Accounts(typing.TypedDict): 24 | mint: Pubkey 25 | 26 | 27 | def initialize_mint2( 28 | args: InitializeMint2Args, 29 | accounts: InitializeMint2Accounts, 30 | program_id: Pubkey = PROGRAM_ID, 31 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 32 | ) -> Instruction: 33 | keys: list[AccountMeta] = [ 34 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True) 35 | ] 36 | if remaining_accounts is not None: 37 | keys += remaining_accounts 38 | identifier = b"_l\xc6\xd2H\xf3\x8f\xeb" 39 | encoded_args = layout.build( 40 | { 41 | "decimals": args["decimals"], 42 | "mint_authority": args["mint_authority"], 43 | "freeze_authority": args["freeze_authority"], 44 | } 45 | ) 46 | data = identifier + encoded_args 47 | return Instruction(program_id, data, keys) 48 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_multisig.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.sysvar import RENT 5 | from solders.instruction import Instruction, AccountMeta 6 | import borsh_construct as borsh 7 | from ..program_id import PROGRAM_ID 8 | 9 | 10 | class InitializeMultisigArgs(typing.TypedDict): 11 | m: int 12 | 13 | 14 | layout = borsh.CStruct("m" / borsh.U8) 15 | 16 | 17 | class InitializeMultisigAccounts(typing.TypedDict): 18 | multisig: Pubkey 19 | 20 | 21 | def initialize_multisig( 22 | args: InitializeMultisigArgs, 23 | accounts: InitializeMultisigAccounts, 24 | program_id: Pubkey = PROGRAM_ID, 25 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 26 | ) -> Instruction: 27 | keys: list[AccountMeta] = [ 28 | AccountMeta(pubkey=accounts["multisig"], is_signer=False, is_writable=True), 29 | AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), 30 | ] 31 | if remaining_accounts is not None: 32 | keys += remaining_accounts 33 | identifier = b"\xdc\x82u\x15\x1b\xe3N\xd5" 34 | encoded_args = layout.build( 35 | { 36 | "m": args["m"], 37 | } 38 | ) 39 | data = identifier + encoded_args 40 | return Instruction(program_id, data, keys) 41 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/initialize_multisig2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class InitializeMultisig2Args(typing.TypedDict): 10 | m: int 11 | 12 | 13 | layout = borsh.CStruct("m" / borsh.U8) 14 | 15 | 16 | class InitializeMultisig2Accounts(typing.TypedDict): 17 | multisig: Pubkey 18 | signer: Pubkey 19 | 20 | 21 | def initialize_multisig2( 22 | args: InitializeMultisig2Args, 23 | accounts: InitializeMultisig2Accounts, 24 | program_id: Pubkey = PROGRAM_ID, 25 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 26 | ) -> Instruction: 27 | keys: list[AccountMeta] = [ 28 | AccountMeta(pubkey=accounts["multisig"], is_signer=False, is_writable=True), 29 | AccountMeta(pubkey=accounts["signer"], is_signer=False, is_writable=False), 30 | ] 31 | if remaining_accounts is not None: 32 | keys += remaining_accounts 33 | identifier = b"Q\xefI'\x1b\x94\x02\x92" 34 | encoded_args = layout.build( 35 | { 36 | "m": args["m"], 37 | } 38 | ) 39 | data = identifier + encoded_args 40 | return Instruction(program_id, data, keys) 41 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/mint_to.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class MintToArgs(typing.TypedDict): 10 | amount: int 11 | 12 | 13 | layout = borsh.CStruct("amount" / borsh.U64) 14 | 15 | 16 | class MintToAccounts(typing.TypedDict): 17 | mint: Pubkey 18 | account: Pubkey 19 | owner: Pubkey 20 | 21 | 22 | def mint_to( 23 | args: MintToArgs, 24 | accounts: MintToAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 32 | ] 33 | if remaining_accounts is not None: 34 | keys += remaining_accounts 35 | identifier = b'\xf1"0\xba%\xb3{\xc0' 36 | encoded_args = layout.build( 37 | { 38 | "amount": args["amount"], 39 | } 40 | ) 41 | data = identifier + encoded_args 42 | return Instruction(program_id, data, keys) 43 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/mint_to_checked.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class MintToCheckedArgs(typing.TypedDict): 10 | amount: int 11 | decimals: int 12 | 13 | 14 | layout = borsh.CStruct("amount" / borsh.U64, "decimals" / borsh.U8) 15 | 16 | 17 | class MintToCheckedAccounts(typing.TypedDict): 18 | mint: Pubkey 19 | account: Pubkey 20 | owner: Pubkey 21 | 22 | 23 | def mint_to_checked( 24 | args: MintToCheckedArgs, 25 | accounts: MintToCheckedAccounts, 26 | program_id: Pubkey = PROGRAM_ID, 27 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 28 | ) -> Instruction: 29 | keys: list[AccountMeta] = [ 30 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 32 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 33 | ] 34 | if remaining_accounts is not None: 35 | keys += remaining_accounts 36 | identifier = b"\xe5\xec$\xf0v\xe1-}" 37 | encoded_args = layout.build( 38 | { 39 | "amount": args["amount"], 40 | "decimals": args["decimals"], 41 | } 42 | ) 43 | data = identifier + encoded_args 44 | return Instruction(program_id, data, keys) 45 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/revoke.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class RevokeAccounts(typing.TypedDict): 9 | source: Pubkey 10 | owner: Pubkey 11 | 12 | 13 | def revoke( 14 | accounts: RevokeAccounts, 15 | program_id: Pubkey = PROGRAM_ID, 16 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 17 | ) -> Instruction: 18 | keys: list[AccountMeta] = [ 19 | AccountMeta(pubkey=accounts["source"], is_signer=False, is_writable=True), 20 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 21 | ] 22 | if remaining_accounts is not None: 23 | keys += remaining_accounts 24 | identifier = b'\xaa\x17\x1f"\x85\xad]\xf2' 25 | encoded_args = b"" 26 | data = identifier + encoded_args 27 | return Instruction(program_id, data, keys) 28 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/set_authority.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from anchorpy.borsh_extension import BorshPubkey, COption 6 | import borsh_construct as borsh 7 | from .. import types 8 | from ..program_id import PROGRAM_ID 9 | 10 | 11 | class SetAuthorityArgs(typing.TypedDict): 12 | authority_type: types.authority_type.AuthorityTypeKind 13 | new_authority: typing.Optional[Pubkey] 14 | 15 | 16 | layout = borsh.CStruct( 17 | "authority_type" / types.authority_type.layout, 18 | "new_authority" / COption(BorshPubkey), 19 | ) 20 | 21 | 22 | class SetAuthorityAccounts(typing.TypedDict): 23 | owned: Pubkey 24 | owner: Pubkey 25 | signer: Pubkey 26 | 27 | 28 | def set_authority( 29 | args: SetAuthorityArgs, 30 | accounts: SetAuthorityAccounts, 31 | program_id: Pubkey = PROGRAM_ID, 32 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 33 | ) -> Instruction: 34 | keys: list[AccountMeta] = [ 35 | AccountMeta(pubkey=accounts["owned"], is_signer=False, is_writable=True), 36 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 37 | AccountMeta(pubkey=accounts["signer"], is_signer=True, is_writable=False), 38 | ] 39 | if remaining_accounts is not None: 40 | keys += remaining_accounts 41 | identifier = b"\x85\xfa%\x15n\xa3\x1ay" 42 | encoded_args = layout.build( 43 | { 44 | "authority_type": args["authority_type"].to_encodable(), 45 | "new_authority": args["new_authority"], 46 | } 47 | ) 48 | data = identifier + encoded_args 49 | return Instruction(program_id, data, keys) 50 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/sync_native.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class SyncNativeAccounts(typing.TypedDict): 9 | account: Pubkey 10 | 11 | 12 | def sync_native( 13 | accounts: SyncNativeAccounts, 14 | program_id: Pubkey = PROGRAM_ID, 15 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 16 | ) -> Instruction: 17 | keys: list[AccountMeta] = [ 18 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True) 19 | ] 20 | if remaining_accounts is not None: 21 | keys += remaining_accounts 22 | identifier = b"\x9b\xdb$$\xef\x80\x15A" 23 | encoded_args = b"" 24 | data = identifier + encoded_args 25 | return Instruction(program_id, data, keys) 26 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/thaw_account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | from ..program_id import PROGRAM_ID 6 | 7 | 8 | class ThawAccountAccounts(typing.TypedDict): 9 | account: Pubkey 10 | mint: Pubkey 11 | owner: Pubkey 12 | 13 | 14 | def thaw_account( 15 | accounts: ThawAccountAccounts, 16 | program_id: Pubkey = PROGRAM_ID, 17 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 18 | ) -> Instruction: 19 | keys: list[AccountMeta] = [ 20 | AccountMeta(pubkey=accounts["account"], is_signer=False, is_writable=True), 21 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 22 | AccountMeta(pubkey=accounts["owner"], is_signer=True, is_writable=False), 23 | ] 24 | if remaining_accounts is not None: 25 | keys += remaining_accounts 26 | identifier = b"s\x98O\xd5\xd5\xa9\xb8#" 27 | encoded_args = b"" 28 | data = identifier + encoded_args 29 | return Instruction(program_id, data, keys) 30 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/transfer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class TransferArgs(typing.TypedDict): 10 | amount: int 11 | 12 | 13 | layout = borsh.CStruct("amount" / borsh.U64) 14 | 15 | 16 | class TransferAccounts(typing.TypedDict): 17 | source: Pubkey 18 | destination: Pubkey 19 | authority: Pubkey 20 | 21 | 22 | def transfer( 23 | args: TransferArgs, 24 | accounts: TransferAccounts, 25 | program_id: Pubkey = PROGRAM_ID, 26 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 27 | ) -> Instruction: 28 | keys: list[AccountMeta] = [ 29 | AccountMeta(pubkey=accounts["source"], is_signer=False, is_writable=True), 30 | AccountMeta(pubkey=accounts["destination"], is_signer=False, is_writable=True), 31 | AccountMeta(pubkey=accounts["authority"], is_signer=True, is_writable=False), 32 | ] 33 | if remaining_accounts is not None: 34 | keys += remaining_accounts 35 | identifier = b"\xa34\xc8\xe7\x8c\x03E\xba" 36 | encoded_args = layout.build( 37 | { 38 | "amount": args["amount"], 39 | } 40 | ) 41 | data = identifier + encoded_args 42 | return Instruction(program_id, data, keys) 43 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/transfer_checked.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class TransferCheckedArgs(typing.TypedDict): 10 | amount: int 11 | decimals: int 12 | 13 | 14 | layout = borsh.CStruct("amount" / borsh.U64, "decimals" / borsh.U8) 15 | 16 | 17 | class TransferCheckedAccounts(typing.TypedDict): 18 | source: Pubkey 19 | mint: Pubkey 20 | destination: Pubkey 21 | authority: Pubkey 22 | 23 | 24 | def transfer_checked( 25 | args: TransferCheckedArgs, 26 | accounts: TransferCheckedAccounts, 27 | program_id: Pubkey = PROGRAM_ID, 28 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 29 | ) -> Instruction: 30 | keys: list[AccountMeta] = [ 31 | AccountMeta(pubkey=accounts["source"], is_signer=False, is_writable=True), 32 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False), 33 | AccountMeta(pubkey=accounts["destination"], is_signer=False, is_writable=True), 34 | AccountMeta(pubkey=accounts["authority"], is_signer=True, is_writable=False), 35 | ] 36 | if remaining_accounts is not None: 37 | keys += remaining_accounts 38 | identifier = b"w\xfa\xca\x18\xfd\x87\xf4y" 39 | encoded_args = layout.build( 40 | { 41 | "amount": args["amount"], 42 | "decimals": args["decimals"], 43 | } 44 | ) 45 | data = identifier + encoded_args 46 | return Instruction(program_id, data, keys) 47 | -------------------------------------------------------------------------------- /tests/client_gen/token/instructions/ui_amount_to_amount.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from solders.pubkey import Pubkey 4 | from solders.instruction import Instruction, AccountMeta 5 | import borsh_construct as borsh 6 | from ..program_id import PROGRAM_ID 7 | 8 | 9 | class UiAmountToAmountArgs(typing.TypedDict): 10 | ui_amount: str 11 | 12 | 13 | layout = borsh.CStruct(borsh.String) 14 | 15 | 16 | class UiAmountToAmountAccounts(typing.TypedDict): 17 | mint: Pubkey 18 | 19 | 20 | def ui_amount_to_amount( 21 | args: UiAmountToAmountArgs, 22 | accounts: UiAmountToAmountAccounts, 23 | program_id: Pubkey = PROGRAM_ID, 24 | remaining_accounts: typing.Optional[typing.List[AccountMeta]] = None, 25 | ) -> Instruction: 26 | keys: list[AccountMeta] = [ 27 | AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False) 28 | ] 29 | if remaining_accounts is not None: 30 | keys += remaining_accounts 31 | identifier = b"\xad\xf3@\x04g\x1f84" 32 | encoded_args = layout.build( 33 | { 34 | "ui_amount": args["ui_amount"], 35 | } 36 | ) 37 | data = identifier + encoded_args 38 | return Instruction(program_id, data, keys) 39 | -------------------------------------------------------------------------------- /tests/client_gen/token/program_id.py: -------------------------------------------------------------------------------- 1 | from solders.pubkey import Pubkey 2 | 3 | PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") 4 | -------------------------------------------------------------------------------- /tests/client_gen/token/types/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from . import account_state 3 | from .account_state import AccountStateKind, AccountStateJSON 4 | from . import authority_type 5 | from .authority_type import AuthorityTypeKind, AuthorityTypeJSON 6 | -------------------------------------------------------------------------------- /tests/client_gen/token/types/account_state.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from anchorpy.borsh_extension import EnumForCodegen 5 | import borsh_construct as borsh 6 | 7 | 8 | class UninitializedJSON(typing.TypedDict): 9 | kind: typing.Literal["Uninitialized"] 10 | 11 | 12 | class InitializedJSON(typing.TypedDict): 13 | kind: typing.Literal["Initialized"] 14 | 15 | 16 | class FrozenJSON(typing.TypedDict): 17 | kind: typing.Literal["Frozen"] 18 | 19 | 20 | @dataclass 21 | class Uninitialized: 22 | discriminator: typing.ClassVar = 0 23 | kind: typing.ClassVar = "Uninitialized" 24 | 25 | @classmethod 26 | def to_json(cls) -> UninitializedJSON: 27 | return UninitializedJSON( 28 | kind="Uninitialized", 29 | ) 30 | 31 | @classmethod 32 | def to_encodable(cls) -> dict: 33 | return { 34 | "Uninitialized": {}, 35 | } 36 | 37 | 38 | @dataclass 39 | class Initialized: 40 | discriminator: typing.ClassVar = 1 41 | kind: typing.ClassVar = "Initialized" 42 | 43 | @classmethod 44 | def to_json(cls) -> InitializedJSON: 45 | return InitializedJSON( 46 | kind="Initialized", 47 | ) 48 | 49 | @classmethod 50 | def to_encodable(cls) -> dict: 51 | return { 52 | "Initialized": {}, 53 | } 54 | 55 | 56 | @dataclass 57 | class Frozen: 58 | discriminator: typing.ClassVar = 2 59 | kind: typing.ClassVar = "Frozen" 60 | 61 | @classmethod 62 | def to_json(cls) -> FrozenJSON: 63 | return FrozenJSON( 64 | kind="Frozen", 65 | ) 66 | 67 | @classmethod 68 | def to_encodable(cls) -> dict: 69 | return { 70 | "Frozen": {}, 71 | } 72 | 73 | 74 | AccountStateKind = typing.Union[Uninitialized, Initialized, Frozen] 75 | AccountStateJSON = typing.Union[UninitializedJSON, InitializedJSON, FrozenJSON] 76 | 77 | 78 | def from_decoded(obj: dict) -> AccountStateKind: 79 | if not isinstance(obj, dict): 80 | raise ValueError("Invalid enum object") 81 | if "Uninitialized" in obj: 82 | return Uninitialized() 83 | if "Initialized" in obj: 84 | return Initialized() 85 | if "Frozen" in obj: 86 | return Frozen() 87 | raise ValueError("Invalid enum object") 88 | 89 | 90 | def from_json(obj: AccountStateJSON) -> AccountStateKind: 91 | if obj["kind"] == "Uninitialized": 92 | return Uninitialized() 93 | if obj["kind"] == "Initialized": 94 | return Initialized() 95 | if obj["kind"] == "Frozen": 96 | return Frozen() 97 | kind = obj["kind"] 98 | raise ValueError(f"Unrecognized enum kind: {kind}") 99 | 100 | 101 | layout = EnumForCodegen( 102 | "Uninitialized" / borsh.CStruct(), 103 | "Initialized" / borsh.CStruct(), 104 | "Frozen" / borsh.CStruct(), 105 | ) 106 | -------------------------------------------------------------------------------- /tests/client_gen/token/types/authority_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing 3 | from dataclasses import dataclass 4 | from anchorpy.borsh_extension import EnumForCodegen 5 | import borsh_construct as borsh 6 | 7 | 8 | class MintTokensJSON(typing.TypedDict): 9 | kind: typing.Literal["MintTokens"] 10 | 11 | 12 | class FreezeAccountJSON(typing.TypedDict): 13 | kind: typing.Literal["FreezeAccount"] 14 | 15 | 16 | class AccountOwnerJSON(typing.TypedDict): 17 | kind: typing.Literal["AccountOwner"] 18 | 19 | 20 | class CloseAccountJSON(typing.TypedDict): 21 | kind: typing.Literal["CloseAccount"] 22 | 23 | 24 | @dataclass 25 | class MintTokens: 26 | discriminator: typing.ClassVar = 0 27 | kind: typing.ClassVar = "MintTokens" 28 | 29 | @classmethod 30 | def to_json(cls) -> MintTokensJSON: 31 | return MintTokensJSON( 32 | kind="MintTokens", 33 | ) 34 | 35 | @classmethod 36 | def to_encodable(cls) -> dict: 37 | return { 38 | "MintTokens": {}, 39 | } 40 | 41 | 42 | @dataclass 43 | class FreezeAccount: 44 | discriminator: typing.ClassVar = 1 45 | kind: typing.ClassVar = "FreezeAccount" 46 | 47 | @classmethod 48 | def to_json(cls) -> FreezeAccountJSON: 49 | return FreezeAccountJSON( 50 | kind="FreezeAccount", 51 | ) 52 | 53 | @classmethod 54 | def to_encodable(cls) -> dict: 55 | return { 56 | "FreezeAccount": {}, 57 | } 58 | 59 | 60 | @dataclass 61 | class AccountOwner: 62 | discriminator: typing.ClassVar = 2 63 | kind: typing.ClassVar = "AccountOwner" 64 | 65 | @classmethod 66 | def to_json(cls) -> AccountOwnerJSON: 67 | return AccountOwnerJSON( 68 | kind="AccountOwner", 69 | ) 70 | 71 | @classmethod 72 | def to_encodable(cls) -> dict: 73 | return { 74 | "AccountOwner": {}, 75 | } 76 | 77 | 78 | @dataclass 79 | class CloseAccount: 80 | discriminator: typing.ClassVar = 3 81 | kind: typing.ClassVar = "CloseAccount" 82 | 83 | @classmethod 84 | def to_json(cls) -> CloseAccountJSON: 85 | return CloseAccountJSON( 86 | kind="CloseAccount", 87 | ) 88 | 89 | @classmethod 90 | def to_encodable(cls) -> dict: 91 | return { 92 | "CloseAccount": {}, 93 | } 94 | 95 | 96 | AuthorityTypeKind = typing.Union[MintTokens, FreezeAccount, AccountOwner, CloseAccount] 97 | AuthorityTypeJSON = typing.Union[ 98 | MintTokensJSON, FreezeAccountJSON, AccountOwnerJSON, CloseAccountJSON 99 | ] 100 | 101 | 102 | def from_decoded(obj: dict) -> AuthorityTypeKind: 103 | if not isinstance(obj, dict): 104 | raise ValueError("Invalid enum object") 105 | if "MintTokens" in obj: 106 | return MintTokens() 107 | if "FreezeAccount" in obj: 108 | return FreezeAccount() 109 | if "AccountOwner" in obj: 110 | return AccountOwner() 111 | if "CloseAccount" in obj: 112 | return CloseAccount() 113 | raise ValueError("Invalid enum object") 114 | 115 | 116 | def from_json(obj: AuthorityTypeJSON) -> AuthorityTypeKind: 117 | if obj["kind"] == "MintTokens": 118 | return MintTokens() 119 | if obj["kind"] == "FreezeAccount": 120 | return FreezeAccount() 121 | if obj["kind"] == "AccountOwner": 122 | return AccountOwner() 123 | if obj["kind"] == "CloseAccount": 124 | return CloseAccount() 125 | kind = obj["kind"] 126 | raise ValueError(f"Unrecognized enum kind: {kind}") 127 | 128 | 129 | layout = EnumForCodegen( 130 | "MintTokens" / borsh.CStruct(), 131 | "FreezeAccount" / borsh.CStruct(), 132 | "AccountOwner" / borsh.CStruct(), 133 | "CloseAccount" / borsh.CStruct(), 134 | ) 135 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest config.""" 2 | import asyncio 3 | import subprocess 4 | from pathlib import Path 5 | 6 | from pytest import fixture 7 | 8 | # Since our other fixtures have module scope, we need to define 9 | # this event_loop fixture and give it module scope otherwise 10 | # pytest-asyncio will break. 11 | 12 | 13 | @fixture(scope="module") 14 | def event_loop(): 15 | """Create an instance of the default event loop for each test case.""" 16 | loop = asyncio.get_event_loop_policy().new_event_loop() 17 | yield loop 18 | loop.close() 19 | 20 | 21 | @fixture(scope="session") 22 | def project_parent_dir(tmpdir_factory) -> Path: 23 | return Path(tmpdir_factory.mktemp("temp")) 24 | 25 | 26 | @fixture(scope="session") 27 | def project_dir(project_parent_dir: Path) -> Path: 28 | proj_dir = project_parent_dir / "tmp" 29 | command = ( 30 | f"anchorpy client-gen tests/idls/clientgen_example_program.json {proj_dir} " 31 | "--program-id 3rTQ3R4B2PxZrAyx7EUefySPgZY8RhJf16cZajbmrzp8 --pdas" 32 | ) 33 | subprocess.run( 34 | command, 35 | shell=True, 36 | check=True, 37 | ) 38 | return proj_dir 39 | -------------------------------------------------------------------------------- /tests/idls/basic_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "basic_0", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [], 8 | "args": [] 9 | } 10 | ], 11 | "metadata": { 12 | "address": "8nA4T4UFdYz1sKuEFdnj2mYcJ6hp79Tw14sPvTQVWwVb" 13 | } 14 | } -------------------------------------------------------------------------------- /tests/idls/basic_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "basic_1", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "myAccount", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "rent", 15 | "isMut": false, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "data", 22 | "type": "u64" 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "update", 28 | "accounts": [ 29 | { 30 | "name": "myAccount", 31 | "isMut": true, 32 | "isSigner": false 33 | } 34 | ], 35 | "args": [ 36 | { 37 | "name": "data", 38 | "type": "u64" 39 | } 40 | ] 41 | } 42 | ], 43 | "accounts": [ 44 | { 45 | "name": "MyAccount", 46 | "type": { 47 | "kind": "struct", 48 | "fields": [ 49 | { 50 | "name": "data", 51 | "type": "u64" 52 | } 53 | ] 54 | } 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /tests/idls/basic_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "basic_2", 4 | "instructions": [ 5 | { 6 | "name": "create", 7 | "accounts": [ 8 | { 9 | "name": "counter", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "rent", 15 | "isMut": false, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "authority", 22 | "type": "publicKey" 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "increment", 28 | "accounts": [ 29 | { 30 | "name": "counter", 31 | "isMut": true, 32 | "isSigner": false 33 | }, 34 | { 35 | "name": "authority", 36 | "isMut": false, 37 | "isSigner": true 38 | } 39 | ], 40 | "args": [] 41 | } 42 | ], 43 | "accounts": [ 44 | { 45 | "name": "Counter", 46 | "type": { 47 | "kind": "struct", 48 | "fields": [ 49 | { 50 | "name": "authority", 51 | "type": "publicKey" 52 | }, 53 | { 54 | "name": "count", 55 | "type": "u64" 56 | } 57 | ] 58 | } 59 | } 60 | ], 61 | "metadata": { 62 | "address": "4V6T2muqU1mHSg5XfK3SMgxZWQRSEUh72LzwGTLHGzsY" 63 | } 64 | } -------------------------------------------------------------------------------- /tests/idls/basic_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "basic_5", 4 | "instructions": [ 5 | { 6 | "name": "createMint", 7 | "accounts": [ 8 | { 9 | "name": "mint", 10 | "isMut": true, 11 | "isSigner": false 12 | } 13 | ], 14 | "args": [] 15 | }, 16 | { 17 | "name": "createToken", 18 | "accounts": [ 19 | { 20 | "name": "token", 21 | "isMut": true, 22 | "isSigner": false 23 | }, 24 | { 25 | "name": "authority", 26 | "isMut": true, 27 | "isSigner": true 28 | }, 29 | { 30 | "name": "mint", 31 | "isMut": false, 32 | "isSigner": false 33 | }, 34 | { 35 | "name": "systemProgram", 36 | "isMut": false, 37 | "isSigner": false 38 | } 39 | ], 40 | "args": [] 41 | } 42 | ], 43 | "accounts": [ 44 | { 45 | "name": "Mint", 46 | "type": { 47 | "kind": "struct", 48 | "fields": [ 49 | { 50 | "name": "supply", 51 | "type": "u32" 52 | } 53 | ] 54 | } 55 | }, 56 | { 57 | "name": "Token", 58 | "type": { 59 | "kind": "struct", 60 | "fields": [ 61 | { 62 | "name": "amount", 63 | "type": "u32" 64 | }, 65 | { 66 | "name": "authority", 67 | "type": "publicKey" 68 | }, 69 | { 70 | "name": "mint", 71 | "type": "publicKey" 72 | } 73 | ] 74 | } 75 | } 76 | ], 77 | "metadata": { 78 | "address": "99cGumFqPFhCLTUxs9BoyhXAV6c19Ca44BYZVn6KG1Pu" 79 | } 80 | } -------------------------------------------------------------------------------- /tests/idls/chat.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "chat", 4 | "instructions": [ 5 | { 6 | "name": "createUser", 7 | "accounts": [ 8 | { 9 | "name": "user", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "authority", 15 | "isMut": false, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | } 23 | ], 24 | "args": [ 25 | { 26 | "name": "name", 27 | "type": "string" 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "createChatRoom", 33 | "accounts": [ 34 | { 35 | "name": "chatRoom", 36 | "isMut": true, 37 | "isSigner": false 38 | } 39 | ], 40 | "args": [ 41 | { 42 | "name": "name", 43 | "type": "string" 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "sendMessage", 49 | "accounts": [ 50 | { 51 | "name": "user", 52 | "isMut": false, 53 | "isSigner": false 54 | }, 55 | { 56 | "name": "authority", 57 | "isMut": false, 58 | "isSigner": true 59 | }, 60 | { 61 | "name": "chatRoom", 62 | "isMut": true, 63 | "isSigner": false 64 | } 65 | ], 66 | "args": [ 67 | { 68 | "name": "msg", 69 | "type": "string" 70 | } 71 | ] 72 | } 73 | ], 74 | "accounts": [ 75 | { 76 | "name": "User", 77 | "type": { 78 | "kind": "struct", 79 | "fields": [ 80 | { 81 | "name": "name", 82 | "type": "string" 83 | }, 84 | { 85 | "name": "authority", 86 | "type": "publicKey" 87 | } 88 | ] 89 | } 90 | }, 91 | { 92 | "name": "ChatRoom", 93 | "type": { 94 | "kind": "struct", 95 | "fields": [ 96 | { 97 | "name": "head", 98 | "type": "u64" 99 | }, 100 | { 101 | "name": "tail", 102 | "type": "u64" 103 | }, 104 | { 105 | "name": "name", 106 | "type": { 107 | "array": [ 108 | "u8", 109 | 280 110 | ] 111 | } 112 | }, 113 | { 114 | "name": "messages", 115 | "type": { 116 | "array": [ 117 | { 118 | "defined": "Message" 119 | }, 120 | 33607 121 | ] 122 | } 123 | } 124 | ] 125 | } 126 | } 127 | ], 128 | "types": [ 129 | { 130 | "name": "Message", 131 | "type": { 132 | "kind": "struct", 133 | "fields": [ 134 | { 135 | "name": "from", 136 | "type": "publicKey" 137 | }, 138 | { 139 | "name": "data", 140 | "type": { 141 | "array": [ 142 | "u8", 143 | 280 144 | ] 145 | } 146 | } 147 | ] 148 | } 149 | } 150 | ], 151 | "errors": [ 152 | { 153 | "code": 300, 154 | "name": "Unknown" 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /tests/idls/composite.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "composite", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "dummyA", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "dummyB", 15 | "isMut": true, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [] 20 | }, 21 | { 22 | "name": "compositeUpdate", 23 | "accounts": [ 24 | { 25 | "name": "foo", 26 | "accounts": [ 27 | { 28 | "name": "dummyA", 29 | "isMut": true, 30 | "isSigner": false 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "bar", 36 | "accounts": [ 37 | { 38 | "name": "dummyB", 39 | "isMut": true, 40 | "isSigner": false 41 | } 42 | ] 43 | } 44 | ], 45 | "args": [ 46 | { 47 | "name": "dummyA", 48 | "type": "u64" 49 | }, 50 | { 51 | "name": "dummyB", 52 | "type": "u64" 53 | } 54 | ] 55 | } 56 | ], 57 | "accounts": [ 58 | { 59 | "name": "DummyA", 60 | "type": { 61 | "kind": "struct", 62 | "fields": [ 63 | { 64 | "name": "data", 65 | "type": "u64" 66 | } 67 | ] 68 | } 69 | }, 70 | { 71 | "name": "DummyB", 72 | "type": { 73 | "kind": "struct", 74 | "fields": [ 75 | { 76 | "name": "data", 77 | "type": "u64" 78 | } 79 | ] 80 | } 81 | } 82 | ], 83 | "metadata": { 84 | "address": "FYuVUPk3hmpT44GjCTL6w8Z3kYQoYBAWZMXEi4P63xto" 85 | } 86 | } -------------------------------------------------------------------------------- /tests/idls/counter_auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "counter_auth", 4 | "instructions": [] 5 | } -------------------------------------------------------------------------------- /tests/idls/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "errors", 4 | "instructions": [ 5 | { 6 | "name": "hello", 7 | "accounts": [], 8 | "args": [] 9 | }, 10 | { 11 | "name": "helloNoMsg", 12 | "accounts": [], 13 | "args": [] 14 | }, 15 | { 16 | "name": "helloNext", 17 | "accounts": [], 18 | "args": [] 19 | }, 20 | { 21 | "name": "mutError", 22 | "accounts": [ 23 | { 24 | "name": "myAccount", 25 | "isMut": true, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [] 30 | }, 31 | { 32 | "name": "hasOneError", 33 | "accounts": [ 34 | { 35 | "name": "myAccount", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "owner", 41 | "isMut": false, 42 | "isSigner": false 43 | } 44 | ], 45 | "args": [] 46 | }, 47 | { 48 | "name": "signerError", 49 | "accounts": [ 50 | { 51 | "name": "myAccount", 52 | "isMut": false, 53 | "isSigner": true 54 | } 55 | ], 56 | "args": [] 57 | } 58 | ], 59 | "accounts": [ 60 | { 61 | "name": "HasOneAccount", 62 | "type": { 63 | "kind": "struct", 64 | "fields": [ 65 | { 66 | "name": "owner", 67 | "type": "publicKey" 68 | } 69 | ] 70 | } 71 | } 72 | ], 73 | "errors": [ 74 | { 75 | "code": 300, 76 | "name": "Hello", 77 | "msg": "This is an error message clients will automatically display" 78 | }, 79 | { 80 | "code": 423, 81 | "name": "HelloNoMsg" 82 | }, 83 | { 84 | "code": 424, 85 | "name": "HelloNext" 86 | } 87 | ] 88 | } -------------------------------------------------------------------------------- /tests/idls/escrow.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "escrow", 4 | "instructions": [ 5 | { 6 | "name": "initializeEscrow", 7 | "accounts": [ 8 | { 9 | "name": "initializer", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "initializerDepositTokenAccount", 15 | "isMut": true, 16 | "isSigner": false 17 | }, 18 | { 19 | "name": "initializerReceiveTokenAccount", 20 | "isMut": false, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "escrowAccount", 25 | "isMut": true, 26 | "isSigner": false 27 | }, 28 | { 29 | "name": "tokenProgram", 30 | "isMut": false, 31 | "isSigner": false 32 | } 33 | ], 34 | "args": [ 35 | { 36 | "name": "initializerAmount", 37 | "type": "u64" 38 | }, 39 | { 40 | "name": "takerAmount", 41 | "type": "u64" 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "cancelEscrow", 47 | "accounts": [ 48 | { 49 | "name": "initializer", 50 | "isMut": false, 51 | "isSigner": false 52 | }, 53 | { 54 | "name": "pdaDepositTokenAccount", 55 | "isMut": true, 56 | "isSigner": false 57 | }, 58 | { 59 | "name": "pdaAccount", 60 | "isMut": false, 61 | "isSigner": false 62 | }, 63 | { 64 | "name": "escrowAccount", 65 | "isMut": true, 66 | "isSigner": false 67 | }, 68 | { 69 | "name": "tokenProgram", 70 | "isMut": false, 71 | "isSigner": false 72 | } 73 | ], 74 | "args": [] 75 | }, 76 | { 77 | "name": "exchange", 78 | "accounts": [ 79 | { 80 | "name": "taker", 81 | "isMut": false, 82 | "isSigner": true 83 | }, 84 | { 85 | "name": "takerDepositTokenAccount", 86 | "isMut": true, 87 | "isSigner": false 88 | }, 89 | { 90 | "name": "takerReceiveTokenAccount", 91 | "isMut": true, 92 | "isSigner": false 93 | }, 94 | { 95 | "name": "pdaDepositTokenAccount", 96 | "isMut": true, 97 | "isSigner": false 98 | }, 99 | { 100 | "name": "initializerReceiveTokenAccount", 101 | "isMut": true, 102 | "isSigner": false 103 | }, 104 | { 105 | "name": "initializerMainAccount", 106 | "isMut": true, 107 | "isSigner": false 108 | }, 109 | { 110 | "name": "escrowAccount", 111 | "isMut": true, 112 | "isSigner": false 113 | }, 114 | { 115 | "name": "pdaAccount", 116 | "isMut": false, 117 | "isSigner": false 118 | }, 119 | { 120 | "name": "tokenProgram", 121 | "isMut": false, 122 | "isSigner": false 123 | } 124 | ], 125 | "args": [] 126 | } 127 | ], 128 | "accounts": [ 129 | { 130 | "name": "EscrowAccount", 131 | "type": { 132 | "kind": "struct", 133 | "fields": [ 134 | { 135 | "name": "initializerKey", 136 | "type": "publicKey" 137 | }, 138 | { 139 | "name": "initializerDepositTokenAccount", 140 | "type": "publicKey" 141 | }, 142 | { 143 | "name": "initializerReceiveTokenAccount", 144 | "type": "publicKey" 145 | }, 146 | { 147 | "name": "initializerAmount", 148 | "type": "u64" 149 | }, 150 | { 151 | "name": "takerAmount", 152 | "type": "u64" 153 | } 154 | ] 155 | } 156 | } 157 | ], 158 | "metadata": { 159 | "address": "38VPDKPpsXerxe7oWCXGQtm5usFGXrf65tbF3A7cs518" 160 | } 161 | } -------------------------------------------------------------------------------- /tests/idls/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "events", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [], 8 | "args": [] 9 | } 10 | ], 11 | "events": [ 12 | { 13 | "name": "MyEvent", 14 | "fields": [ 15 | { 16 | "name": "data", 17 | "type": "u64", 18 | "index": false 19 | }, 20 | { 21 | "name": "label", 22 | "type": "string", 23 | "index": true 24 | } 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /tests/idls/jet_auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "jet_auth", 4 | "instructions": [ 5 | { 6 | "name": "createUserAuth", 7 | "accounts": [ 8 | { 9 | "name": "user", 10 | "isMut": false, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "payer", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "auth", 20 | "isMut": true, 21 | "isSigner": false 22 | }, 23 | { 24 | "name": "systemProgram", 25 | "isMut": false, 26 | "isSigner": false 27 | } 28 | ], 29 | "args": [], 30 | "returns": null 31 | }, 32 | { 33 | "name": "authenticate", 34 | "accounts": [ 35 | { 36 | "name": "auth", 37 | "isMut": true, 38 | "isSigner": false 39 | }, 40 | { 41 | "name": "authority", 42 | "isMut": false, 43 | "isSigner": true 44 | }, 45 | { 46 | "name": "authority", 47 | "isMut": false, 48 | "isSigner": false 49 | } 50 | ], 51 | "args": [], 52 | "returns": null 53 | } 54 | ], 55 | "accounts": [ 56 | { 57 | "name": "UserAuthentication", 58 | "type": { 59 | "kind": "struct", 60 | "fields": [ 61 | { 62 | "name": "owner", 63 | "type": "publicKey" 64 | }, 65 | { 66 | "name": "complete", 67 | "type": "bool" 68 | }, 69 | { 70 | "name": "allowed", 71 | "type": "bool" 72 | } 73 | ] 74 | } 75 | } 76 | ], 77 | "events": [ 78 | { 79 | "name": "AuthAccountCreated", 80 | "fields": [ 81 | { 82 | "name": "user", 83 | "type": "publicKey", 84 | "index": false 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "Authenticated", 90 | "fields": [ 91 | { 92 | "name": "user", 93 | "type": "publicKey", 94 | "index": false 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /tests/idls/puppet.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "puppet", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "puppet", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "rent", 15 | "isMut": false, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [] 20 | }, 21 | { 22 | "name": "setData", 23 | "accounts": [ 24 | { 25 | "name": "puppet", 26 | "isMut": true, 27 | "isSigner": false 28 | } 29 | ], 30 | "args": [ 31 | { 32 | "name": "data", 33 | "type": "u64" 34 | } 35 | ] 36 | } 37 | ], 38 | "accounts": [ 39 | { 40 | "name": "Puppet", 41 | "type": { 42 | "kind": "struct", 43 | "fields": [ 44 | { 45 | "name": "data", 46 | "type": "u64" 47 | } 48 | ] 49 | } 50 | } 51 | ], 52 | "metadata": { 53 | "address": "61Q5bAAha3NF2gowifnZxaRttM1eyCs4Y3j9YjgMWrqb" 54 | } 55 | } -------------------------------------------------------------------------------- /tests/idls/puppet_master.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "puppet_master", 4 | "instructions": [ 5 | { 6 | "name": "pullStrings", 7 | "accounts": [ 8 | { 9 | "name": "puppet", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "puppetProgram", 15 | "isMut": false, 16 | "isSigner": false 17 | } 18 | ], 19 | "args": [ 20 | { 21 | "name": "data", 22 | "type": "u64" 23 | } 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "address": "3UDzsDD9WWPAgHASFHyFdqZz1hFmWpWVCZzLPZabDJxe" 29 | } 30 | } -------------------------------------------------------------------------------- /tests/idls/pyth.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "pyth", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [ 8 | { 9 | "name": "price", 10 | "isMut": true, 11 | "isSigner": false 12 | } 13 | ], 14 | "args": [ 15 | { 16 | "name": "price", 17 | "type": "i64" 18 | }, 19 | { 20 | "name": "expo", 21 | "type": "i32" 22 | }, 23 | { 24 | "name": "conf", 25 | "type": "u64" 26 | } 27 | ] 28 | }, 29 | { 30 | "name": "setPrice", 31 | "accounts": [ 32 | { 33 | "name": "price", 34 | "isMut": true, 35 | "isSigner": false 36 | } 37 | ], 38 | "args": [ 39 | { 40 | "name": "price", 41 | "type": "i64" 42 | } 43 | ] 44 | } 45 | ], 46 | "types": [ 47 | { 48 | "name": "PriceStatus", 49 | "type": { 50 | "kind": "enum", 51 | "variants": [ 52 | { 53 | "name": "Unknown" 54 | }, 55 | { 56 | "name": "Trading" 57 | }, 58 | { 59 | "name": "Halted" 60 | }, 61 | { 62 | "name": "Auction" 63 | } 64 | ] 65 | } 66 | }, 67 | { 68 | "name": "CorpAction", 69 | "type": { 70 | "kind": "enum", 71 | "variants": [ 72 | { 73 | "name": "NoCorpAct" 74 | } 75 | ] 76 | } 77 | }, 78 | { 79 | "name": "PriceType", 80 | "type": { 81 | "kind": "enum", 82 | "variants": [ 83 | { 84 | "name": "Unknown" 85 | }, 86 | { 87 | "name": "Price" 88 | }, 89 | { 90 | "name": "TWAP" 91 | }, 92 | { 93 | "name": "Volatility" 94 | } 95 | ] 96 | } 97 | } 98 | ] 99 | } -------------------------------------------------------------------------------- /tests/idls/sysvars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sysvars", 4 | "instructions": [ 5 | { 6 | "name": "sysvars", 7 | "accounts": [ 8 | { 9 | "name": "clock", 10 | "isMut": false, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "rent", 15 | "isMut": false, 16 | "isSigner": false 17 | }, 18 | { 19 | "name": "stakeHistory", 20 | "isMut": false, 21 | "isSigner": false 22 | } 23 | ], 24 | "args": [] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /tests/idls/tictactoe.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "tic_tac_toe", 4 | "instructions": [ 5 | { 6 | "name": "setupGame", 7 | "accounts": [ 8 | { 9 | "name": "game", 10 | "isMut": true, 11 | "isSigner": true 12 | }, 13 | { 14 | "name": "playerOne", 15 | "isMut": true, 16 | "isSigner": true 17 | }, 18 | { 19 | "name": "systemProgram", 20 | "isMut": false, 21 | "isSigner": false 22 | } 23 | ], 24 | "args": [ 25 | { 26 | "name": "playerTwo", 27 | "type": "publicKey" 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "play", 33 | "accounts": [ 34 | { 35 | "name": "game", 36 | "isMut": true, 37 | "isSigner": false 38 | }, 39 | { 40 | "name": "player", 41 | "isMut": false, 42 | "isSigner": true 43 | } 44 | ], 45 | "args": [ 46 | { 47 | "name": "tile", 48 | "type": { 49 | "defined": "Tile" 50 | } 51 | } 52 | ] 53 | } 54 | ], 55 | "accounts": [ 56 | { 57 | "name": "Game", 58 | "type": { 59 | "kind": "struct", 60 | "fields": [ 61 | { 62 | "name": "players", 63 | "type": { 64 | "array": ["publicKey", 2] 65 | } 66 | }, 67 | { 68 | "name": "turn", 69 | "type": "u8" 70 | }, 71 | { 72 | "name": "board", 73 | "type": { 74 | "array": [ 75 | { 76 | "array": [ 77 | { 78 | "option": { 79 | "defined": "Sign" 80 | } 81 | }, 82 | 3 83 | ] 84 | }, 85 | 3 86 | ] 87 | } 88 | }, 89 | { 90 | "name": "state", 91 | "type": { 92 | "defined": "GameState" 93 | } 94 | } 95 | ] 96 | } 97 | } 98 | ], 99 | "types": [ 100 | { 101 | "name": "Tile", 102 | "type": { 103 | "kind": "struct", 104 | "fields": [ 105 | { 106 | "name": "row", 107 | "type": "u8" 108 | }, 109 | { 110 | "name": "column", 111 | "type": "u8" 112 | } 113 | ] 114 | } 115 | }, 116 | { 117 | "name": "GameState", 118 | "type": { 119 | "kind": "enum", 120 | "variants": [ 121 | { 122 | "name": "Active" 123 | }, 124 | { 125 | "name": "Tie" 126 | }, 127 | { 128 | "name": "Won", 129 | "fields": [ 130 | { 131 | "name": "winner", 132 | "type": "publicKey" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | }, 139 | { 140 | "name": "Sign", 141 | "type": { 142 | "kind": "enum", 143 | "variants": [ 144 | { 145 | "name": "X" 146 | }, 147 | { 148 | "name": "O" 149 | } 150 | ] 151 | } 152 | } 153 | ], 154 | "errors": [ 155 | { 156 | "code": 6000, 157 | "name": "TileOutOfBounds" 158 | }, 159 | { 160 | "code": 6001, 161 | "name": "TileAlreadySet" 162 | }, 163 | { 164 | "code": 6002, 165 | "name": "GameAlreadyOver" 166 | }, 167 | { 168 | "code": 6003, 169 | "name": "NotPlayersTurn" 170 | }, 171 | { 172 | "code": 6004, 173 | "name": "GameAlreadyStarted" 174 | } 175 | ], 176 | "metadata": { 177 | "address": "4V6T2muqU1mHSg5XfK3SMgxZWQRSEUh72LzwGTLHGzsY" 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /tests/idls/typescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "typescript", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "accounts": [], 8 | "args": [] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Test that the CLI commands work.""" 2 | 3 | from pathlib import Path 4 | 5 | from anchorpy import localnet_fixture 6 | from anchorpy.cli import app 7 | from solana.rpc.api import Client 8 | from solana.rpc.commitment import Processed 9 | from solders.signature import Signature 10 | from typer.testing import CliRunner 11 | 12 | PATH = Path("anchor/examples/tutorial/basic-0") 13 | 14 | localnet = localnet_fixture(PATH) 15 | 16 | runner = CliRunner() 17 | 18 | 19 | def test_shell(localnet, monkeypatch) -> None: 20 | monkeypatch.chdir("anchor/examples/tutorial/basic-0") 21 | cli_input = "await workspace['basic_0'].rpc['initialize']()\nexit()" 22 | result = runner.invoke(app, ["shell"], input=cli_input) 23 | assert result.exit_code == 0 24 | assert "Hint: type `workspace`" in result.stdout 25 | tx_sig = Signature.from_string( 26 | result.stdout.split("Out[1]: \n")[1].split("\n ")[1].split(",")[0] 27 | ) 28 | client = Client() 29 | client.confirm_transaction(tx_sig, commitment=Processed) 30 | -------------------------------------------------------------------------------- /tests/unit/test_accounts_array.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import Idl 4 | from anchorpy.program.namespace.instruction import _accounts_array 5 | from pytest import mark 6 | from solders.instruction import AccountMeta 7 | from solders.keypair import Keypair 8 | 9 | 10 | @mark.unit 11 | def test_accounts_array() -> None: 12 | """Test accounts_array returns expected.""" 13 | raw = Path("tests/idls/composite.json").read_text() 14 | idl = Idl.from_json(raw) 15 | dummy_a = Keypair() 16 | dummy_b = Keypair() 17 | comp_accounts = { 18 | "foo": { 19 | "dummy_a": dummy_a.pubkey(), 20 | }, 21 | "bar": { 22 | "dummy_b": dummy_b.pubkey(), 23 | }, 24 | } 25 | accounts_arg = idl.instructions[1].accounts 26 | acc_arr = _accounts_array(comp_accounts, accounts_arg) 27 | assert acc_arr == [ 28 | AccountMeta(pubkey=dummy_a.pubkey(), is_signer=False, is_writable=True), 29 | AccountMeta(pubkey=dummy_b.pubkey(), is_signer=False, is_writable=True), 30 | ] 31 | -------------------------------------------------------------------------------- /tests/unit/test_accounts_coder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import AccountsCoder, Idl 4 | from pytest import mark 5 | 6 | 7 | @mark.unit 8 | def test_accounts_coder() -> None: 9 | """Test accounts coder.""" 10 | raw = Path("tests/idls/basic_1.json").read_text() 11 | idl = Idl.from_json(raw) 12 | raw_acc_data = b"\xf6\x1c\x06W\xfb-2*\xd2\x04\x00\x00\x00\x00\x00\x00" 13 | acc_coder = AccountsCoder(idl) 14 | decoded = acc_coder.parse(raw_acc_data) 15 | encoded = acc_coder.build(decoded) 16 | assert encoded == raw_acc_data 17 | -------------------------------------------------------------------------------- /tests/unit/test_clientgen.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import Idl 4 | from anchorpy.clientgen.instructions import gen_accounts 5 | from anchorpy.clientgen.types import gen_struct 6 | from genpy import Suite 7 | 8 | 9 | def test_gen_accounts() -> None: 10 | path = Path("tests/idls/composite.json") 11 | raw = path.read_text() 12 | idl = Idl.from_json(raw) 13 | accs = gen_accounts( 14 | "CompositeUpdateAccounts", idl.instructions[1].accounts, gen_pdas=True 15 | )[0] 16 | suite = Suite(accs) 17 | assert str(suite) == ( 18 | " class CompositeUpdateAccounts(typing.TypedDict):" 19 | "\n foo: FooNested" 20 | "\n bar: BarNested" 21 | "\n class FooNested(typing.TypedDict):" 22 | "\n dummy_a: Pubkey" 23 | "\n class BarNested(typing.TypedDict):" 24 | "\n dummy_b: Pubkey" 25 | "\n class FooNested(typing.TypedDict):" 26 | "\n dummy_a: Pubkey" 27 | ) 28 | 29 | 30 | def test_empty_fields() -> None: 31 | path = Path("tests/idls/switchboard_v2.mainnet.06022022.json") 32 | raw = path.read_text() 33 | idl = Idl.from_json(raw) 34 | struct = gen_struct(idl, "AggregatorLockParams", []) 35 | assert str(struct) == ( 36 | "import typing" 37 | "\nfrom dataclasses import dataclass" 38 | "\nfrom construct import Container, Construct" 39 | "\nfrom solders.pubkey import Pubkey" 40 | "\nfrom anchorpy.borsh_extension import BorshPubkey" 41 | "\nimport borsh_construct as borsh" 42 | "\nclass AggregatorLockParamsJSON(typing.TypedDict):" 43 | "\n pass" 44 | "\n@dataclass" 45 | "\nclass AggregatorLockParams():" 46 | "\n layout: typing.ClassVar = borsh.CStruct()" 47 | "\n @classmethod" 48 | '\n def from_decoded(cls, obj: Container) -> "AggregatorLockParams":' 49 | "\n return cls()" 50 | "\n def to_encodable(self) -> dict[str, typing.Any]:" 51 | "\n return {}" 52 | "\n def to_json(self) -> AggregatorLockParamsJSON:" 53 | "\n return {}" 54 | "\n @classmethod" 55 | '\n def from_json(cls, obj: AggregatorLockParamsJSON) -> "AggregatorLockParams":' 56 | "\n return cls()" 57 | ) 58 | -------------------------------------------------------------------------------- /tests/unit/test_event_parser.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import Event, EventParser, Idl, Program 4 | from solders.pubkey import Pubkey 5 | 6 | 7 | def test_event_parser() -> None: 8 | path = Path("tests/idls/events.json") 9 | raw = path.read_text() 10 | idl = Idl.from_json(raw) 11 | program = Program( 12 | idl, Pubkey.from_string("2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy") 13 | ) 14 | logs = [ 15 | "Program 2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy invoke [1]", 16 | "Program log: Instruction: Initialize", 17 | "Program data: YLjF84sCWpQFAAAAAAAAAAUAAABoZWxsbw==", 18 | "Program 2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy consumed 1019 of 1400000 compute units", 19 | "Program 2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy success", 20 | ] 21 | parser = EventParser(program.program_id, program.coder) 22 | evts = [] 23 | parser.parse_logs(logs, lambda evt: evts.append(evt)) 24 | assert len(evts) == 1 25 | events_coder = program.coder.events 26 | event_cls = events_coder.layouts["MyEvent"].datacls # type: ignore 27 | expected_data = event_cls( 28 | data=5, 29 | label="hello", 30 | ) 31 | expected_event = Event(name="MyEvent", data=expected_data) 32 | assert evts[0] == expected_event 33 | -------------------------------------------------------------------------------- /tests/unit/test_idl.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import Idl, Program 4 | from solders.pubkey import Pubkey 5 | 6 | 7 | def test_idls() -> None: 8 | idls = [] 9 | programs = [] 10 | for path in Path("tests/idls/").iterdir(): 11 | raw = path.read_text() 12 | idl = Idl.from_json(raw) 13 | idls.append(idl) 14 | if "spl_token" not in str(path): 15 | program = Program(idl, Pubkey.default()) 16 | programs.append(program) 17 | assert idls 18 | 19 | 20 | def test_jet_enum() -> None: 21 | path = Path("tests/idls/jet.json") 22 | raw = path.read_text() 23 | idl = Idl.from_json(raw) 24 | program = Program(idl, Pubkey.default()) 25 | expired_err = program.type["CacheInvalidError"].Expired 26 | assert expired_err(msg="hi").msg == "hi" 27 | 28 | 29 | def test_switchboard_tuple() -> None: 30 | path = Path("tests/idls/switchboard.json") 31 | raw = path.read_text() 32 | idl = Idl.from_json(raw) 33 | program = Program(idl, Pubkey.default()) # noqa: F841 34 | 35 | 36 | def test_clientgen_example() -> None: 37 | path = Path("tests/idls/clientgen_example_program.json") 38 | raw = path.read_text() 39 | Idl.from_json(raw) 40 | -------------------------------------------------------------------------------- /tests/unit/test_instruction_coder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from anchorpy import Idl, InstructionCoder 4 | from anchorpy.program.common import _to_instruction 5 | from anchorpy.program.context import _check_args_length 6 | from pytest import mark 7 | 8 | 9 | @mark.unit 10 | def test_instruction_coder() -> None: 11 | """Test InstructionCoder behaves as expected.""" 12 | raw = Path("tests/idls/basic_1.json").read_text() 13 | idl = Idl.from_json(raw) 14 | idl_ix = idl.instructions[0] 15 | args = (1234,) 16 | _check_args_length(idl_ix, args) 17 | ix = _to_instruction(idl_ix, args) 18 | coder = InstructionCoder(idl) 19 | encoded = coder.build(ix) 20 | assert encoded == b"\xaf\xafm\x1f\r\x98\x9b\xed\xd2\x04\x00\x00\x00\x00\x00\x00" 21 | assert coder.parse(encoded) == ix 22 | -------------------------------------------------------------------------------- /tests/unit/test_transaction.py: -------------------------------------------------------------------------------- 1 | from anchorpy import Coder, Idl 2 | from anchorpy.program.context import Context 3 | from anchorpy.program.namespace.instruction import _InstructionFn 4 | from anchorpy.program.namespace.transaction import _build_transaction_fn 5 | from pytest import fixture 6 | from solders.hash import Hash 7 | from solders.instruction import Instruction 8 | from solders.keypair import Keypair 9 | from solders.pubkey import Pubkey 10 | 11 | DEFAULT_PUBKEY = Pubkey.default() 12 | 13 | 14 | def _make_ix(data: bytes) -> Instruction: 15 | return Instruction( 16 | accounts=[], 17 | program_id=DEFAULT_PUBKEY, 18 | data=data, 19 | ) 20 | 21 | 22 | @fixture 23 | def pre_ix() -> Instruction: 24 | return _make_ix(b"pre") 25 | 26 | 27 | @fixture 28 | def post_ix() -> Instruction: 29 | return _make_ix(b"post") 30 | 31 | 32 | @fixture 33 | def idl() -> Idl: 34 | raw = """{ 35 | "version": "0.0.0", 36 | "name": "basic_0", 37 | "instructions": [ 38 | { 39 | "name": "initialize", 40 | "accounts": [], 41 | "args": [] 42 | } 43 | ] 44 | }""" 45 | return Idl.from_json(raw) 46 | 47 | 48 | @fixture 49 | def coder(idl: Idl) -> Coder: 50 | return Coder(idl) 51 | 52 | 53 | def test_pre_instructions(coder: Coder, idl: Idl, pre_ix: Instruction) -> None: 54 | coder.instruction.encode 55 | ix_item = _InstructionFn( 56 | idl.instructions[0], coder.instruction.build, DEFAULT_PUBKEY 57 | ) 58 | tx_item = _build_transaction_fn(idl.instructions[0], ix_item) 59 | tx = tx_item( 60 | ctx=Context(pre_instructions=[pre_ix]), 61 | payer=Keypair(), 62 | blockhash=Hash.default(), 63 | ) 64 | assert len(tx.message.instructions) == 2 65 | 66 | 67 | def test_post_instructions(coder: Coder, idl: Idl, post_ix: Instruction) -> None: 68 | coder.instruction.encode 69 | ix_item = _InstructionFn( 70 | idl.instructions[0], coder.instruction.build, DEFAULT_PUBKEY 71 | ) 72 | tx_item = _build_transaction_fn(idl.instructions[0], ix_item) 73 | tx = tx_item( 74 | ctx=Context(post_instructions=[post_ix]), 75 | payer=Keypair(), 76 | blockhash=Hash.default(), 77 | ) 78 | assert len(tx.message.instructions) == 2 79 | -------------------------------------------------------------------------------- /tests/unit/test_types_coder.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from anchorpy import Coder 5 | from anchorpy_core.idl import Idl 6 | 7 | 8 | @pytest.mark.unit 9 | def test_can_encode_and_decode_user_defined_types(): 10 | """Test that the TypesCoder can encode and decode user-defined types.""" 11 | idl_json = { 12 | "version": "0.0.0", 13 | "name": "basic_0", 14 | "address": "Test111111111111111111111111111111111111111", 15 | "instructions": [ 16 | { 17 | "name": "initialize", 18 | "accounts": [], 19 | "args": [], 20 | "discriminator": [], 21 | }, 22 | ], 23 | "types": [ 24 | { 25 | "name": "MintInfo", 26 | "type": { 27 | "kind": "struct", 28 | "fields": [ 29 | { 30 | "name": "minted", 31 | "type": "bool", 32 | }, 33 | { 34 | "name": "metadataUrl", 35 | "type": "string", 36 | }, 37 | ], 38 | }, 39 | }, 40 | ], 41 | } 42 | idl = Idl.from_json(json.dumps(idl_json)) 43 | coder = Coder(idl) 44 | 45 | mint_info = { 46 | "minted": True, 47 | "metadata_url": "hello", 48 | } 49 | encoded = coder.types.encode("MintInfo", mint_info) 50 | decoded = coder.types.decode("MintInfo", encoded) 51 | 52 | # Compare decoded values with original 53 | assert decoded.minted == mint_info["minted"] 54 | assert decoded.metadata_url == mint_info["metadata_url"] 55 | 56 | 57 | @pytest.mark.unit 58 | def test_can_encode_and_decode_large_integers(): 59 | """Test that the TypesCoder can encode and decode 128-bit integers.""" 60 | idl_json = { 61 | "version": "0.0.0", 62 | "name": "basic_0", 63 | "address": "Test111111111111111111111111111111111111111", 64 | "instructions": [ 65 | { 66 | "name": "initialize", 67 | "accounts": [], 68 | "args": [], 69 | "discriminator": [], 70 | }, 71 | ], 72 | "types": [ 73 | { 74 | "name": "IntegerTest", 75 | "type": { 76 | "kind": "struct", 77 | "fields": [ 78 | { 79 | "name": "unsigned", 80 | "type": "u128", 81 | }, 82 | { 83 | "name": "signed", 84 | "type": "i128", 85 | }, 86 | ], 87 | }, 88 | }, 89 | ], 90 | } 91 | idl = Idl.from_json(json.dumps(idl_json)) 92 | coder = Coder(idl) 93 | 94 | integer_test = { 95 | "unsigned": 2588012355, 96 | "signed": -93842345, 97 | } 98 | encoded = coder.types.encode("IntegerTest", integer_test) 99 | decoded = coder.types.decode("IntegerTest", encoded) 100 | 101 | assert decoded.unsigned == integer_test["unsigned"] 102 | assert decoded.signed == integer_test["signed"] 103 | --------------------------------------------------------------------------------