├── .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 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 | [](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 |
--------------------------------------------------------------------------------