├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── python-setup │ │ └── action.yaml ├── scripts │ └── fail_if_modified_files.sh └── workflows │ ├── devnet-examples.yaml │ ├── lints.yaml │ ├── localnet-examples.yaml │ └── publish.yaml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── Makefile ├── README.md ├── aptos_sdk ├── __init__.py ├── account.py ├── account_address.py ├── account_sequence_number.py ├── aptos_cli_wrapper.py ├── aptos_token_client.py ├── aptos_tokenv1_client.py ├── asymmetric_crypto.py ├── asymmetric_crypto_wrapper.py ├── async_client.py ├── authenticator.py ├── bcs.py ├── cli.py ├── ed25519.py ├── metadata.py ├── package_publisher.py ├── py.typed ├── secp256k1_ecdsa.py ├── transaction_worker.py ├── transactions.py └── type_tag.py ├── examples ├── .gitignore ├── __init__.py ├── aptos_token.py ├── common.py ├── fee_payer_transfer_coin.py ├── hello_blockchain.py ├── integration_test.py ├── large_package_publisher.py ├── multikey.py ├── multisig.py ├── object_code_deployment.py ├── read_aggregator.py ├── rotate_key.py ├── secp256k1_ecdsa_transfer_coin.py ├── simple_aptos_token.py ├── simple_nft.py ├── simulate_transfer_coin.py ├── transaction_batching.py ├── transfer_coin.py ├── transfer_two_by_two.py ├── two_by_two_transfer.mv └── your_coin.py ├── features ├── account_address.feature ├── bcs_deserialization.feature ├── bcs_serialization.feature ├── steps │ ├── account_address.py │ ├── bcs.py │ └── common.py └── stubs │ └── behave │ ├── __init__.pyi │ ├── _stepimport.pyi │ ├── _types.pyi │ ├── api │ ├── __init__.pyi │ └── async_step.pyi │ ├── capture.pyi │ ├── compat │ ├── __init__.pyi │ └── collections.pyi │ ├── configuration.pyi │ ├── contrib │ ├── __init__.pyi │ ├── formatter_missing_steps.pyi │ ├── scenario_autoretry.pyi │ ├── steps │ │ └── __init__.pyi │ └── substep_dirs.pyi │ ├── fixture.pyi │ ├── formatter │ ├── __init__.pyi │ ├── _builtins.pyi │ ├── _registry.pyi │ ├── ansi_escapes.pyi │ ├── base.pyi │ ├── formatters.pyi │ ├── json.pyi │ ├── null.pyi │ ├── plain.pyi │ ├── pretty.pyi │ ├── progress.pyi │ ├── rerun.pyi │ ├── sphinx_steps.pyi │ ├── sphinx_util.pyi │ ├── steps.pyi │ └── tags.pyi │ ├── i18n.pyi │ ├── importer.pyi │ ├── json_parser.pyi │ ├── log_capture.pyi │ ├── matchers.pyi │ ├── model.pyi │ ├── model_core.pyi │ ├── model_describe.pyi │ ├── parser.pyi │ ├── reporter │ ├── __init__.pyi │ ├── base.pyi │ ├── junit.pyi │ └── summary.pyi │ ├── runner.pyi │ ├── runner_util.pyi │ ├── step_registry.pyi │ ├── tag_expression.pyi │ ├── tag_matcher.pyi │ ├── textutil.pyi │ └── userdata.pyi ├── mypy.ini ├── poetry.lock └── pyproject.toml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | select = C,E,F,W,B,B9 4 | ignore = E203, E501, W503, E701, E704 5 | exclude = __init__.py 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a bug report to help improve the Aptos Typescript SDK 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🐛 Bug Description 11 | 12 | 16 | 17 | ## How to reproduce 18 | 19 | **Code snippet to reproduce** 20 | ```typescript 21 | // Your code goes here 22 | // Please make sure to list any dependencies 23 | ``` 24 | 25 | **Stack trace / error message** 26 | ``` 27 | // Paste the output here 28 | ``` 29 | 30 | ## Expected Behavior 31 | 32 | 35 | 36 | ## System information 37 | 38 | **System details:** 39 | - Typescript SDK Version 40 | - Platform (e.g. Node, browser, etc.) 41 | 42 | ## Additional context 43 | 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions and Help (on Aptos Developer Discussions) 4 | url: (https://github.com/aptos-labs/aptos-developer-discussions/discussions 5 | about: Support and other Questions are handled by the team and the community on Aptos Developer Discussions. 6 | - name: Questions, Help and Chat (on Discord) 7 | url: https://discord.gg/aptosnetwork 8 | about: Contact the development team, contributors and community on Discord 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest a new feature in the Aptos Typescript SDK 4 | title: "[Feature Request] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🚀 Feature Request Description 11 | 12 | 13 | 14 | ## Motivation 15 | 16 | **Is your feature request related to a problem? Please describe you use case.** 17 | 18 | 19 | 20 | 21 | ## Pitch 22 | 23 | **Describe the solution you would like** 24 | 25 | 26 | **Describe alternatives you've considered** 27 | 28 | 29 | **Are you willing to open a pull request?** (See [CONTRIBUTING](../../CONTRIBUTING.md)) 30 | 31 | ## Additional context 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | ### Test Plan 5 | 6 | 7 | ### Related Links 8 | -------------------------------------------------------------------------------- /.github/actions/python-setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Python setup" 2 | description: | 3 | Runs an opinionated and unified python setup action. It does the following: 4 | * Installs python 5 | * Installs poetry 6 | * Installs a specified poetry project, if given 7 | inputs: 8 | pyproject_directory: 9 | description: "Optional path to a poetry project" 10 | required: false 11 | 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Setup python 16 | uses: actions/setup-python@v4 17 | 18 | # Install Poetry. 19 | - uses: snok/install-poetry@d45b6d76012debf457ab49dffc7fb7b2efe8071d # pin@v1.3.3 20 | with: 21 | version: 2.1.3 22 | 23 | - name: Install poetry project 24 | if: inputs.pyproject_directory != '' 25 | run: poetry install 26 | shell: bash 27 | working-directory: ${{ inputs.pyproject_directory }} 28 | -------------------------------------------------------------------------------- /.github/scripts/fail_if_modified_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # fail if there are modified files 4 | if [[ -n $(git status --porcelain --untracked-files=no) ]]; then 5 | echo "Failure: there are modified files" 6 | git status 7 | git diff 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /.github/workflows/devnet-examples.yaml: -------------------------------------------------------------------------------- 1 | name: "Devnet Examples" 2 | on: 3 | pull_request: 4 | types: [labeled, opened, synchronize, reopened, auto_merge_enabled] 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | run-devnet-examples: 11 | runs-on: ubuntu-latest 12 | env: 13 | APTOS_FAUCET_URL: https://faucet.devnet.aptoslabs.com 14 | APTOS_INDEXER_URL: https://api.devnet.aptoslabs.com/v1/graphql 15 | APTOS_NODE_URL: https://fullnode.devnet.aptoslabs.com/v1 16 | FAUCET_AUTH_TOKEN: ${{ secrets.DEVNET_FAUCET_AUTH_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: ./.github/actions/python-setup 21 | with: 22 | pyproject_directory: . 23 | 24 | - name: Run examples on devnet 25 | uses: nick-fields/retry@v3 26 | with: 27 | max_attempts: 1 28 | timeout_minutes: 20 29 | command: make examples 30 | -------------------------------------------------------------------------------- /.github/workflows/lints.yaml: -------------------------------------------------------------------------------- 1 | name: "Local validation" 2 | permissions: 3 | contents: read 4 | on: 5 | pull_request: 6 | types: [labeled, opened, synchronize, reopened, auto_merge_enabled] 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lints: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Run python setup 18 | uses: ./.github/actions/python-setup 19 | with: 20 | pyproject_directory: . 21 | 22 | - name: Check for formatting 23 | run: make fmt && ./.github/scripts/fail_if_modified_files.sh 24 | shell: bash 25 | 26 | - name: Check lints 27 | run: make lint 28 | shell: bash 29 | 30 | - name: Run integration tests 31 | run: make test 32 | shell: bash 33 | -------------------------------------------------------------------------------- /.github/workflows/localnet-examples.yaml: -------------------------------------------------------------------------------- 1 | name: "Localnet Examples" 2 | permissions: 3 | contents: read 4 | on: 5 | pull_request: 6 | types: [labeled, opened, synchronize, reopened, auto_merge_enabled] 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-localnet-examples: 13 | runs-on: ubuntu-latest 14 | env: 15 | APTOS_CORE_PATH: ./aptos-core 16 | APTOS_FAUCET_URL: http://127.0.0.1:8081 17 | APTOS_INDEXER_URL: http://127.0.0.1:8090/v1/graphql 18 | APTOS_NODE_URL: http://127.0.0.1:8080/v1 19 | APTOS_TEST_USE_EXISTING_NETWORK: true 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Checkout aptos-core 25 | uses: actions/checkout@v4 26 | with: 27 | repository: aptos-labs/aptos-core 28 | path: './aptos-core' 29 | 30 | - name: Run a localnet 31 | uses: aptos-labs/actions/run-local-testnet@main 32 | with: 33 | node_version: v20.12.2 34 | pnpm_version: 8.15.6 35 | 36 | - uses: ./.github/actions/python-setup 37 | with: 38 | pyproject_directory: . 39 | 40 | - name: Install Aptos-CLI 41 | run: pnpm install -g @aptos-labs/aptos-cli 42 | shell: bash 43 | 44 | - name: Run examples on localnet 45 | run: make integration_test 46 | shell: bash 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: "Run Python SDK Publish" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | 14 | - uses: ./.github/actions/python-setup 15 | 16 | - name: Build project for distribution 17 | run: poetry build 18 | 19 | - name: Publish to PyPI 20 | env: 21 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 22 | run: poetry publish 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Other stuff 2 | *.egg.info 3 | /myvenv 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | .idea/ 165 | 166 | # testing/fidling script: 167 | 168 | # MacOS 169 | .DS_Store 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Aptos Python SDK Changelog 2 | 3 | All notable changes to the Aptos Python SDK will be captured in this file. This changelog is written by hand for now. 4 | 5 | ## Unreleased 6 | 7 | ## 0.11.0 8 | 9 | - **[Breaking Change]**: `ed25519` and `secp256k1` private key's `__str__` will now return the AIP-80 compliant string 10 | - `PrivateKey.format_private_key` can now format a AIP-80 compliant private key 11 | - Removed strictness warnnings for `PrivateKey.parse_hex_input` 12 | - Make HTTP2 default 13 | - Update all dependencies 14 | - Add ability for `account_balance` with other coins 15 | - Upgrade to Poetry 2.1.3 16 | 17 | ## 0.10.0 18 | 19 | - Added support for deserialize RawTransactionWithData 20 | - Added support for AIP-80 compliance for Ed25519 and Secp256k1 private keys. 21 | - Added helper functions for AIP-80 including `PrivateKey.format_private_key` and `PrivateKey.parse_hex_input` 22 | 23 | ## 0.9.2 24 | - Fix MultiKeyAuthenicator serialization and deserialization with tests 25 | 26 | ## 0.9.1 27 | - For `account_sequence_number`, return 0 if account has yet to be created to better support sponsored transactions create account if not exists 28 | - For `account_balance`, Use `0x1::coin::balance` instead of reading the resource 29 | 30 | ## 0.9.0 31 | - Add Multikey support for Python, with an example 32 | - Deprecate and remove non-BCS transaction submission 33 | - Set max Uleb128 to MAX_U32 34 | - Add Behave behavioral specifications for BCS and AccountAddress 35 | 36 | ## 0.8.6 37 | - add client for graphql indexer service with light demo in coin transfer 38 | - add mypy to ignore missing types for graphql and ecdsa 39 | - remove `<4.0` requirement for python as this invariant blocks updates unnecessarily, for example, httpx was several versions behind 40 | - remove h2 as it doesn't seem to be directly used 41 | - add py.typed so that projects can add type checking when using the sdk 42 | - fix tables api -- there was an extra `base_url` 43 | - ClientConfig updates for bearer token 44 | - Identified a TypeTag parsing issue where nested types weren't wrapped with TypeTag 45 | 46 | ## 0.8.1 47 | - Improve TypeTag parsing for nested types 48 | - Add BCS and String-based (JSON) view functions 49 | - Added thorough documentation 50 | 51 | ## 0.8.0 52 | - Add support for SingleKeyAuthenicatoin component of AIP-55 53 | - Add support for Secp256k1 Ecdsa of AIP-49 54 | - Add support for Sponsored transactions of AIP-39 and AIP-53 55 | - Improved support for MultiEd25519 56 | 57 | ## 0.7.0 58 | - **[Breaking Change]**: The `from_str` function on `AccountAddress` has been updated to conform to the strict parsing described by [AIP-40](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md). For the relaxed parsing behavior of this function prior to this change, use `AccountAddress.from_str_relaxed`. 59 | - **[Breaking Change]**: Rewrote the large package publisher to support large modules too 60 | - **[Breaking Change]**: Delete sync client 61 | - **[Breaking Change]**: Removed the `hex` function from `AccountAddress`. Instead of `addr.hex()` use `str(addr)`. 62 | - **[Breaking Change]**: The string representation of `AccountAddress` now conforms to [AIP-40](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md). 63 | - **[Breaking Change]**: `AccountAddress.from_hex` and `PrivateKey.from_hex` have been renamed to `from_str`. 64 | - Port remaining sync examples to async (hello-blockchain, multisig, your-coin) 65 | - Updated token client to use events to acquire minted tokens 66 | - Update many dependencies and set Python 3.8.1 as the minimum requirement 67 | - Add support for an experimental chunked uploader 68 | - Add experimental support for the Aptos CLI enabling local end-to-end testing, package building, and package integration tests 69 | 70 | ## 0.6.4 71 | - Change sync client library from httpX to requests due to latency concerns. 72 | 73 | ## 0.6.2 74 | - Added custom header "x-aptos-client" to both sync/async RestClient 75 | 76 | ## 0.6.1 77 | - Updated package manifest. 78 | 79 | ## 0.6.0 80 | - Add token client. 81 | - Add support for generating account addresses. 82 | - Add support for http2 83 | - Add async client 84 | 85 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # **Please** keep this file ordered alphabetically by directory paths. 2 | 3 | # Owners for the `.github` directory and all its subdirectories. 4 | 5 | /.github/ @aptos-labs/developer-platform @aptos-labs/security 6 | 7 | # Auto-reviewers for all code 8 | 9 | * @aptos-labs/developer-platform @davidiw @gregnazario 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | ## Publishing 3 | To publish the SDK, follow these steps. 4 | 5 | First, make sure you have updated the changelog and bumped the SDK version if necessary. 6 | 7 | Configure Poetry with the PyPi credentials: 8 | 9 | ``` 10 | poetry config pypi-token.pypi 11 | ``` 12 | 13 | You can get the token from our credential management system, search for PyPi. 14 | 15 | Build and publish: 16 | ``` 17 | poetry publish --build 18 | ``` 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | test: 5 | poetry run python -m unittest discover -s aptos_sdk/ -p '*.py' -t .. 6 | poetry run behave 7 | 8 | test-coverage: 9 | poetry run python -m coverage run -m unittest discover -s aptos_sdk/ -p '*.py' -t .. 10 | poetry run python -m coverage report 11 | 12 | test-spec: 13 | poetry run behave 14 | 15 | fmt: 16 | find ./examples ./aptos_sdk ./features . -type f -name "*.py" | xargs poetry run autoflake -i -r --remove-all-unused-imports --remove-unused-variables --ignore-init-module-imports 17 | poetry run isort aptos_sdk examples features 18 | poetry run black aptos_sdk examples features 19 | 20 | lint: 21 | poetry run mypy aptos_sdk examples features 22 | poetry run flake8 aptos_sdk examples features 23 | 24 | examples: 25 | poetry run python -m examples.aptos_token 26 | poetry run python -m examples.fee_payer_transfer_coin 27 | poetry run python -m examples.multikey 28 | poetry run python -m examples.rotate_key 29 | poetry run python -m examples.read_aggregator 30 | poetry run python -m examples.secp256k1_ecdsa_transfer_coin 31 | poetry run python -m examples.simple_aptos_token 32 | poetry run python -m examples.simple_nft 33 | poetry run python -m examples.simulate_transfer_coin 34 | poetry run python -m examples.transfer_coin 35 | poetry run python -m examples.transfer_two_by_two 36 | 37 | examples_cli: 38 | poetry run python -m examples.hello_blockchain 39 | # poetry run python -m examples.large_package_publisher CURRENTLY BROKEN -- OUT OF GAS 40 | #poetry run python -m examples.multisig CURRENTLY BROKEN requires aptos-core checkout 41 | poetry run python -m examples.object_code_deployment 42 | poetry run python -m examples.your_coin 43 | 44 | integration_test: 45 | poetry run python -m unittest -b examples.integration_test 46 | 47 | .PHONY: examples fmt lint test 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aptos Python SDK 2 | [![Discord][discord-image]][discord-url] 3 | [![PyPI Package Version][pypi-image-version]][pypi-url] 4 | [![PyPI Package Downloads][pypi-image-downloads]][pypi-url] 5 | 6 | This provides basic functionalities to interact with [Aptos](https:/github.com/aptos-labs/aptos-core/). Get started [here](https://aptos.dev/guides/system-integrators-guide/#getting-started). 7 | 8 | Currently, this is still in development and may not be suitable for production purposes. 9 | 10 | Note: The sync client is deprecated, please only start new projects using the async client. Feature contributions to the sync client will be rejected. 11 | 12 | ## Requirements 13 | This SDK uses [Poetry](https://python-poetry.org/docs/#installation) for packaging and dependency management: 14 | 15 | ``` 16 | curl -sSL https://install.python-poetry.org | python3 - 17 | poetry install 18 | ``` 19 | 20 | ## Unit testing 21 | ```bash 22 | make test 23 | ``` 24 | 25 | ## E2E testing and Using the Aptos CLI 26 | 27 | * Download and install the [Aptos CLI](https://aptos.dev/tools/aptos-cli/use-cli/running-a-local-network). 28 | * Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI. 29 | * Retrieve the [Aptos Core Github Repo](https://github.com/aptos-labs/aptos-core) (git clone https://github.com/aptos-labs/aptos-core) 30 | * Set the environment variable `APTOS_CORE_REPO` to the full path of the Repository. 31 | * `make integration_test` 32 | 33 | You can do this a bit more manually by: 34 | 35 | First, run a local testnet (run this from the root of aptos-core): 36 | 37 | ```bash 38 | aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api 39 | ``` 40 | 41 | Next, tell the end-to-end tests to talk to this locally running testnet: 42 | 43 | ```bash 44 | export APTOS_CORE_REPO="/path/to/repo" 45 | export APTOS_FAUCET_URL="http://127.0.0.1:8081" 46 | export APTOS_INDEXER_URL="http://127.0.0.1:8090/v1/graphql" 47 | export APTOS_NODE_URL="http://127.0.0.1:8080/v1" 48 | ``` 49 | 50 | Finally run the tests: 51 | 52 | ```bash 53 | make examples 54 | ``` 55 | 56 | Integration Testing Using the Aptos CLI: 57 | 58 | ```bash 59 | make integration_test 60 | ``` 61 | 62 | > [!NOTE] 63 | > The Python SDK does not require the Indexer, if you would prefer to test without it, unset or do not set the environmental variable `APTOS_INDEXER_URL` and exclude `--with-indexer-api` from running the aptos node software. 64 | 65 | ## Autoformatting 66 | ```bash 67 | make fmt 68 | ``` 69 | 70 | ## Autolinting 71 | ```bash 72 | make lint 73 | ``` 74 | 75 | ## Package Publishing 76 | 77 | * Download the [Aptos CLI](https://aptos.dev/tools/aptos-cli/install-cli/). 78 | * Set the environment variable `APTOS_CLI_PATH` to the full path of the CLI. 79 | * `poetry run python -m aptos_sdk.cli` and set the appropriate command-line parameters 80 | 81 | ## Semantic versioning 82 | This project follows [semver](https://semver.org/) as closely as possible 83 | 84 | [repo]: https://github.com/aptos-labs/aptos-core 85 | [pypi-image-version]: https://img.shields.io/pypi/v/aptos-sdk.svg 86 | [pypi-image-downloads]: https://img.shields.io/pypi/dm/aptos-sdk.svg 87 | [pypi-url]: https://pypi.org/project/aptos-sdk 88 | [discord-image]: https://img.shields.io/discord/945856774056083548?label=Discord&logo=discord&style=flat~~~~ 89 | [discord-url]: https://discord.gg/aptosnetwork 90 | -------------------------------------------------------------------------------- /aptos_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /aptos_sdk/account.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | import json 7 | import tempfile 8 | import unittest 9 | 10 | from . import asymmetric_crypto, asymmetric_crypto_wrapper, ed25519, secp256k1_ecdsa 11 | from .account_address import AccountAddress 12 | from .authenticator import AccountAuthenticator 13 | from .bcs import Serializer 14 | from .transactions import RawTransactionInternal 15 | 16 | 17 | class Account: 18 | """Represents an account as well as the private, public key-pair for the Aptos blockchain.""" 19 | 20 | account_address: AccountAddress 21 | private_key: asymmetric_crypto.PrivateKey 22 | 23 | def __init__( 24 | self, account_address: AccountAddress, private_key: asymmetric_crypto.PrivateKey 25 | ): 26 | self.account_address = account_address 27 | self.private_key = private_key 28 | 29 | def __eq__(self, other: object) -> bool: 30 | if not isinstance(other, Account): 31 | return NotImplemented 32 | return ( 33 | self.account_address == other.account_address 34 | and self.private_key == other.private_key 35 | ) 36 | 37 | @staticmethod 38 | def generate() -> Account: 39 | private_key = ed25519.PrivateKey.random() 40 | account_address = AccountAddress.from_key(private_key.public_key()) 41 | return Account(account_address, private_key) 42 | 43 | @staticmethod 44 | def generate_secp256k1_ecdsa() -> Account: 45 | private_key = secp256k1_ecdsa.PrivateKey.random() 46 | public_key = asymmetric_crypto_wrapper.PublicKey(private_key.public_key()) 47 | account_address = AccountAddress.from_key(public_key) 48 | return Account(account_address, private_key) 49 | 50 | @staticmethod 51 | def load_key(key: str) -> Account: 52 | private_key = ed25519.PrivateKey.from_str(key) 53 | account_address = AccountAddress.from_key(private_key.public_key()) 54 | return Account(account_address, private_key) 55 | 56 | @staticmethod 57 | def load(path: str) -> Account: 58 | with open(path) as file: 59 | data = json.load(file) 60 | return Account( 61 | AccountAddress.from_str_relaxed(data["account_address"]), 62 | ed25519.PrivateKey.from_str(data["private_key"]), 63 | ) 64 | 65 | def store(self, path: str): 66 | data = { 67 | "account_address": str(self.account_address), 68 | "private_key": str(self.private_key), 69 | } 70 | with open(path, "w") as file: 71 | json.dump(data, file) 72 | 73 | def address(self) -> AccountAddress: 74 | """Returns the address associated with the given account""" 75 | 76 | return self.account_address 77 | 78 | def auth_key(self) -> str: 79 | """Returns the auth_key for the associated account""" 80 | return str(AccountAddress.from_key(self.private_key.public_key())) 81 | 82 | def sign(self, data: bytes) -> asymmetric_crypto.Signature: 83 | return self.private_key.sign(data) 84 | 85 | def sign_simulated_transaction( 86 | self, transaction: RawTransactionInternal 87 | ) -> AccountAuthenticator: 88 | return transaction.sign_simulated(self.private_key.public_key()) 89 | 90 | def sign_transaction( 91 | self, transaction: RawTransactionInternal 92 | ) -> AccountAuthenticator: 93 | return transaction.sign(self.private_key) 94 | 95 | def public_key(self) -> asymmetric_crypto.PublicKey: 96 | """Returns the public key for the associated account""" 97 | 98 | return self.private_key.public_key() 99 | 100 | 101 | class RotationProofChallenge: 102 | type_info_account_address: AccountAddress = AccountAddress.from_str("0x1") 103 | type_info_module_name: str = "account" 104 | type_info_struct_name: str = "RotationProofChallenge" 105 | sequence_number: int 106 | originator: AccountAddress 107 | current_auth_key: AccountAddress 108 | new_public_key: asymmetric_crypto.PublicKey 109 | 110 | def __init__( 111 | self, 112 | sequence_number: int, 113 | originator: AccountAddress, 114 | current_auth_key: AccountAddress, 115 | new_public_key: asymmetric_crypto.PublicKey, 116 | ): 117 | self.sequence_number = sequence_number 118 | self.originator = originator 119 | self.current_auth_key = current_auth_key 120 | self.new_public_key = new_public_key 121 | 122 | def serialize(self, serializer: Serializer): 123 | self.type_info_account_address.serialize(serializer) 124 | serializer.str(self.type_info_module_name) 125 | serializer.str(self.type_info_struct_name) 126 | serializer.u64(self.sequence_number) 127 | self.originator.serialize(serializer) 128 | self.current_auth_key.serialize(serializer) 129 | serializer.struct(self.new_public_key) 130 | 131 | 132 | class Test(unittest.TestCase): 133 | def test_load_and_store(self): 134 | (file, path) = tempfile.mkstemp() 135 | start = Account.generate() 136 | start.store(path) 137 | load = Account.load(path) 138 | 139 | self.assertEqual(start, load) 140 | # Auth key and Account address should be the same at start 141 | self.assertEqual(str(start.address()), start.auth_key()) 142 | 143 | def test_key(self): 144 | message = b"test message" 145 | account = Account.generate() 146 | signature = account.sign(message) 147 | self.assertTrue(account.public_key().verify(message, signature)) 148 | 149 | def test_rotation_proof_challenge(self): 150 | # Create originating account from private key. 151 | originating_account = Account.load_key( 152 | "005120c5882b0d492b3d2dc60a8a4510ec2051825413878453137305ba2d644b" 153 | ) 154 | # Create target account from private key. 155 | target_account = Account.load_key( 156 | "19d409c191b1787d5b832d780316b83f6ee219677fafbd4c0f69fee12fdcdcee" 157 | ) 158 | # Construct rotation proof challenge. 159 | rotation_proof_challenge = RotationProofChallenge( 160 | sequence_number=1234, 161 | originator=originating_account.address(), 162 | current_auth_key=originating_account.address(), 163 | new_public_key=target_account.public_key(), 164 | ) 165 | # Serialize transaction. 166 | serializer = Serializer() 167 | rotation_proof_challenge.serialize(serializer) 168 | rotation_proof_challenge_bcs = serializer.output().hex() 169 | # Compare against expected bytes. 170 | expected_bytes = ( 171 | "0000000000000000000000000000000000000000000000000000000000000001" 172 | "076163636f756e7416526f746174696f6e50726f6f664368616c6c656e6765d2" 173 | "0400000000000015b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087" 174 | "bd9076589219c615b67a673979c7c5dfc8d9c9f94d02da35062a19dd9d218087" 175 | "bd9076589219c620a1f942a3c46e2a4cd9552c0f95d529f8e3b60bcd44408637" 176 | "9ace35e4458b9f22" 177 | ) 178 | self.assertEqual(rotation_proof_challenge_bcs, expected_bytes) 179 | -------------------------------------------------------------------------------- /aptos_sdk/account_sequence_number.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | import logging 8 | import unittest 9 | import unittest.mock 10 | from typing import Callable, Optional 11 | 12 | from aptos_sdk.account_address import AccountAddress 13 | from aptos_sdk.async_client import ApiError, RestClient 14 | 15 | 16 | class AccountSequenceNumberConfig: 17 | """Common configuration for account number generation""" 18 | 19 | maximum_in_flight: int = 100 20 | maximum_wait_time: int = 30 21 | sleep_time: float = 0.01 22 | 23 | 24 | class AccountSequenceNumber: 25 | """ 26 | A managed wrapper around sequence numbers that implements the trivial flow control used by the 27 | Aptos faucet: 28 | * Submit up to 100 transactions per account in parallel with a timeout of 20 seconds 29 | * If local assumes 100 are in flight, determine the actual committed state from the network 30 | * If there are less than 100 due to some being committed, adjust the window 31 | * If 100 are in flight Wait .1 seconds before re-evaluating 32 | * If ever waiting more than 30 seconds restart the sequence number to the current on-chain state 33 | Assumptions: 34 | * Accounts are expected to be managed by a single AccountSequenceNumber and not used otherwise. 35 | * They are initialized to the current on-chain state, so if there are already transactions in 36 | flight, they may take some time to reset. 37 | * Accounts are automatically initialized if not explicitly 38 | 39 | Notes: 40 | * This is co-routine safe, that is many async tasks can be reading from this concurrently. 41 | * The state of an account cannot be used across multiple AccountSequenceNumber services. 42 | * The synchronize method will create a barrier that prevents additional next_sequence_number 43 | calls until it is complete. 44 | * This only manages the distribution of sequence numbers it does not help handle transaction 45 | failures. 46 | * If a transaction fails, you should call synchronize and wait for timeouts. 47 | * Mempool limits the number of transactions per account to 100, hence why we chose 100. 48 | """ 49 | 50 | _client: RestClient 51 | _account: AccountAddress 52 | _lock: asyncio.Lock 53 | 54 | _maximum_in_flight: int = 100 55 | _maximum_wait_time: int = 30 56 | _sleep_time: float = 0.01 57 | 58 | _last_committed_number: int = 0 59 | _current_number: int = 0 60 | _initialized = False 61 | 62 | def __init__( 63 | self, 64 | client: RestClient, 65 | account: AccountAddress, 66 | config: AccountSequenceNumberConfig = AccountSequenceNumberConfig(), 67 | ): 68 | self._client = client 69 | self._account = account 70 | self._lock = asyncio.Lock() 71 | 72 | self._maximum_in_flight = config.maximum_in_flight 73 | self._maximum_wait_time = config.maximum_wait_time 74 | self._sleep_time = config.sleep_time 75 | 76 | async def next_sequence_number(self, block: bool = True) -> Optional[int]: 77 | """ 78 | Returns the next sequence number available on this account. This leverages a lock to 79 | guarantee first-in, first-out ordering of requests. 80 | """ 81 | async with self._lock: 82 | if not self._initialized: 83 | await self._initialize() 84 | # If there are more than self._maximum_in_flight in flight, wait for a slot. 85 | # Or at least check to see if there is a slot and exit if in non-blocking mode. 86 | if ( 87 | self._current_number - self._last_uncommitted_number 88 | >= self._maximum_in_flight 89 | ): 90 | await self._update() 91 | if ( 92 | self._current_number - self._last_uncommitted_number 93 | >= self._maximum_in_flight 94 | ): 95 | if not block: 96 | return None 97 | await self._resync( 98 | lambda acn: acn._current_number - acn._last_uncommitted_number 99 | >= acn._maximum_in_flight 100 | ) 101 | 102 | next_number = self._current_number 103 | self._current_number += 1 104 | return next_number 105 | 106 | async def _initialize(self): 107 | """Optional initializer. called by next_sequence_number if not called prior.""" 108 | self._initialized = True 109 | self._current_number = await self._current_sequence_number() 110 | self._last_uncommitted_number = self._current_number 111 | 112 | async def synchronize(self): 113 | """ 114 | Poll the network until all submitted transactions have either been committed or until 115 | the maximum wait time has elapsed. This will prevent any calls to next_sequence_number 116 | until this called has returned. 117 | """ 118 | async with self._lock: 119 | await self._update() 120 | await self._resync( 121 | lambda acn: acn._last_uncommitted_number != acn._current_number 122 | ) 123 | 124 | async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): 125 | """Forces a resync with the upstream, this should be called within the lock""" 126 | start_time = await self._client.current_timestamp() 127 | failed = False 128 | while check(self): 129 | ledger_time = await self._client.current_timestamp() 130 | if ledger_time - start_time > self._maximum_wait_time: 131 | logging.warn( 132 | f"Waited over {self._maximum_wait_time} seconds for a transaction to commit, resyncing {self._account}" 133 | ) 134 | failed = True 135 | break 136 | else: 137 | await asyncio.sleep(self._sleep_time) 138 | await self._update() 139 | if not failed: 140 | return 141 | for seq_num in range(self._last_uncommitted_number + 1, self._current_number): 142 | while True: 143 | try: 144 | result = ( 145 | await self._client.account_transaction_sequence_number_status( 146 | self._account, seq_num 147 | ) 148 | ) 149 | if result: 150 | break 151 | except ApiError as error: 152 | if error.status_code == 404: 153 | break 154 | raise 155 | await self._initialize() 156 | 157 | async def _update(self): 158 | self._last_uncommitted_number = await self._current_sequence_number() 159 | return self._last_uncommitted_number 160 | 161 | async def _current_sequence_number(self) -> int: 162 | return await self._client.account_sequence_number(self._account) 163 | 164 | 165 | class Test(unittest.IsolatedAsyncioTestCase): 166 | async def test_common_path(self): 167 | """ 168 | Verifies that: 169 | * AccountSequenceNumber returns sequential numbers starting from 0 170 | * When the account has been updated on-chain include that in computations 100 -> 105 171 | * Ensure that none is returned if the call for next_sequence_number would block 172 | * Ensure that synchronize completes if the value matches on-chain 173 | """ 174 | patcher = unittest.mock.patch( 175 | "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=0 176 | ) 177 | patcher.start() 178 | 179 | rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") 180 | account_sequence_number = AccountSequenceNumber( 181 | rest_client, AccountAddress.from_str("0xf") 182 | ) 183 | last_seq_num = 0 184 | for seq_num in range(5): 185 | last_seq_num = await account_sequence_number.next_sequence_number() 186 | self.assertEqual(last_seq_num, seq_num) 187 | 188 | patcher.stop() 189 | patcher = unittest.mock.patch( 190 | "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=5 191 | ) 192 | patcher.start() 193 | 194 | for seq_num in range(AccountSequenceNumber._maximum_in_flight): 195 | last_seq_num = await account_sequence_number.next_sequence_number() 196 | self.assertEqual(last_seq_num, seq_num + 5) 197 | 198 | self.assertEqual( 199 | await account_sequence_number.next_sequence_number(block=False), None 200 | ) 201 | next_sequence_number = last_seq_num + 1 202 | patcher.stop() 203 | patcher = unittest.mock.patch( 204 | "aptos_sdk.async_client.RestClient.account_sequence_number", 205 | return_value=next_sequence_number, 206 | ) 207 | patcher.start() 208 | 209 | self.assertNotEqual(account_sequence_number._current_number, last_seq_num) 210 | await account_sequence_number.synchronize() 211 | self.assertEqual(account_sequence_number._current_number, next_sequence_number) 212 | -------------------------------------------------------------------------------- /aptos_sdk/aptos_cli_wrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | import os 8 | import shutil 9 | import subprocess 10 | import tempfile 11 | import threading 12 | import time 13 | from typing import Dict, List 14 | 15 | from .account_address import AccountAddress 16 | from .async_client import FaucetClient, RestClient 17 | 18 | # Assume that the binary is in the global path if one is not provided. 19 | DEFAULT_BINARY = os.getenv("APTOS_CLI_PATH", "aptos") 20 | LOCAL_FAUCET = "http://127.0.0.1:8081" 21 | LOCAL_NODE = "http://127.0.0.1:8080/v1" 22 | 23 | # Assume that the node failed to start if it has been more than this time since the process started 24 | MAXIMUM_WAIT_TIME_SEC = 30 25 | 26 | 27 | class AptosCLIWrapper: 28 | """Tooling to make easy access to the Aptos CLI tool from within Python.""" 29 | 30 | @staticmethod 31 | def prepare_named_addresses( 32 | named_addresses: Dict[str, AccountAddress] 33 | ) -> List[str]: 34 | total_names = len(named_addresses) 35 | args: List[str] = [] 36 | if total_names == 0: 37 | return args 38 | 39 | args.append("--named-addresses") 40 | for idx, (name, addr) in enumerate(named_addresses.items()): 41 | to_append = f"{name}={addr}" 42 | if idx < total_names - 1: 43 | to_append += "," 44 | args.append(to_append) 45 | return args 46 | 47 | @staticmethod 48 | def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): 49 | AptosCLIWrapper.assert_cli_exists() 50 | args = [ 51 | DEFAULT_BINARY, 52 | "move", 53 | "compile", 54 | "--save-metadata", 55 | "--package-dir", 56 | package_dir, 57 | ] 58 | args.extend(AptosCLIWrapper.prepare_named_addresses(named_addresses)) 59 | 60 | process_output = subprocess.run(args, capture_output=True) 61 | if process_output.returncode != 0: 62 | raise CLIError(args, process_output.stdout, process_output.stderr) 63 | 64 | @staticmethod 65 | def start_node() -> AptosInstance: 66 | AptosCLIWrapper.assert_cli_exists() 67 | return AptosInstance.start() 68 | 69 | @staticmethod 70 | def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): 71 | AptosCLIWrapper.assert_cli_exists() 72 | args = [ 73 | DEFAULT_BINARY, 74 | "move", 75 | "test", 76 | "--package-dir", 77 | package_dir, 78 | ] 79 | args.extend(AptosCLIWrapper.prepare_named_addresses(named_addresses)) 80 | 81 | process_output = subprocess.run(args, capture_output=True) 82 | if process_output.returncode != 0: 83 | raise CLIError(args, process_output.stdout, process_output.stderr) 84 | 85 | @staticmethod 86 | def assert_cli_exists(): 87 | if not AptosCLIWrapper.does_cli_exist(): 88 | raise MissingCLIError() 89 | 90 | @staticmethod 91 | def does_cli_exist(): 92 | return shutil.which(DEFAULT_BINARY) is not None 93 | 94 | 95 | class MissingCLIError(Exception): 96 | """The CLI was not found in the expected path.""" 97 | 98 | def __init__(self): 99 | super().__init__("The CLI was not found in the expected path, {DEFAULT_BINARY}") 100 | 101 | 102 | class CLIError(Exception): 103 | """The CLI failed execution of a command.""" 104 | 105 | def __init__(self, command, output, error): 106 | super().__init__( 107 | f"The CLI operation failed:\n\tCommand: {' '.join(command)}\n\tOutput: {output}\n\tError: {error}" 108 | ) 109 | 110 | 111 | class AptosInstance: 112 | """ 113 | A standalone Aptos node running by itself. This still needs a bit of work: 114 | * a test instance should be loaded into its own port space. Currently they share ports as 115 | those are not configurable without a config file. As a result, it is possible that two 116 | test runs may share a single AptosInstance and both successfully complete. 117 | * Probably need some means to monitor the process in case it stops, as we aren't actively 118 | monitoring this. 119 | """ 120 | 121 | _node_runner: subprocess.Popen 122 | _temp_dir: tempfile.TemporaryDirectory 123 | _output: List[str] 124 | _error: List[str] 125 | 126 | def __del__(self): 127 | self.stop() 128 | 129 | def __init__( 130 | self, node_runner: subprocess.Popen, temp_dir: tempfile.TemporaryDirectory 131 | ): 132 | self._node_runner = node_runner 133 | self._temp_dir = temp_dir 134 | 135 | self._output = [] 136 | self._error = [] 137 | 138 | def queue_lines(pipe, target): 139 | for line in iter(pipe.readline, b""): 140 | if line == "": 141 | continue 142 | target.append(line) 143 | pipe.close() 144 | 145 | err_thread = threading.Thread( 146 | target=queue_lines, args=(node_runner.stderr, self._error) 147 | ) 148 | err_thread.daemon = True 149 | err_thread.start() 150 | 151 | out_thread = threading.Thread( 152 | target=queue_lines, args=(node_runner.stdout, self._output) 153 | ) 154 | out_thread.daemon = True 155 | out_thread.start() 156 | 157 | @staticmethod 158 | def start() -> AptosInstance: 159 | temp_dir = tempfile.TemporaryDirectory() 160 | args = [ 161 | DEFAULT_BINARY, 162 | "node", 163 | "run-local-testnet", 164 | "--test-dir", 165 | str(temp_dir), 166 | "--with-faucet", 167 | "--force-restart", 168 | "--assume-yes", 169 | ] 170 | node_runner = subprocess.Popen( 171 | args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True 172 | ) 173 | return AptosInstance(node_runner, temp_dir) 174 | 175 | def stop(self): 176 | self._node_runner.terminate() 177 | self._node_runner.wait() 178 | self._temp_dir.cleanup() 179 | 180 | def errors(self) -> List[str]: 181 | return self._error 182 | 183 | def output(self) -> List[str]: 184 | return self._output 185 | 186 | async def wait_until_operational(self) -> bool: 187 | operational = await self.is_operational() 188 | start = time.time() 189 | last = start 190 | 191 | while ( 192 | not self.is_stopped() 193 | and not operational 194 | and start + MAXIMUM_WAIT_TIME_SEC > last 195 | ): 196 | await asyncio.sleep(0.1) 197 | operational = await self.is_operational() 198 | last = time.time() 199 | return not self.is_stopped() 200 | 201 | async def is_operational(self) -> bool: 202 | rest_client = RestClient(LOCAL_NODE) 203 | faucet_client = FaucetClient(LOCAL_NODE, rest_client) 204 | 205 | try: 206 | await rest_client.chain_id() 207 | return await faucet_client.healthy() 208 | except Exception: 209 | return False 210 | finally: 211 | await rest_client.close() 212 | 213 | def is_stopped(self) -> bool: 214 | return self._node_runner.returncode is not None 215 | -------------------------------------------------------------------------------- /aptos_sdk/asymmetric_crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | from enum import Enum 7 | 8 | from typing_extensions import Protocol 9 | 10 | from .bcs import Deserializable, Serializable 11 | 12 | 13 | class PrivateKeyVariant(Enum): 14 | Ed25519 = "ed25519" 15 | Secp256k1 = "secp256k1" 16 | 17 | 18 | class PrivateKey(Deserializable, Serializable, Protocol): 19 | def hex(self) -> str: ... 20 | 21 | def public_key(self) -> PublicKey: ... 22 | 23 | def sign(self, data: bytes) -> Signature: ... 24 | 25 | """ 26 | The AIP-80 compliant prefixes for each private key type. Append this to a private key's hex representation 27 | to get an AIP-80 compliant string. 28 | 29 | [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) 30 | """ 31 | AIP80_PREFIXES: dict[PrivateKeyVariant, str] = { 32 | PrivateKeyVariant.Ed25519: "ed25519-priv-", 33 | PrivateKeyVariant.Secp256k1: "secp256k1-priv-", 34 | } 35 | 36 | @staticmethod 37 | def format_private_key( 38 | private_key: bytes | str, key_type: PrivateKeyVariant 39 | ) -> str: 40 | """ 41 | Format a HexInput to an AIP-80 compliant string. 42 | 43 | :param private_key: The hex string or bytes format of the private key. 44 | :param key_type: The private key type. 45 | :return: AIP-80 compliant string. 46 | """ 47 | if key_type not in PrivateKey.AIP80_PREFIXES: 48 | raise ValueError(f"Unknown private key type: {key_type}") 49 | aip80_prefix = PrivateKey.AIP80_PREFIXES[key_type] 50 | 51 | key_value: str | None = None 52 | if isinstance(private_key, str): 53 | if private_key.startswith(aip80_prefix): 54 | key_value = private_key.split("-")[2] 55 | else: 56 | key_value = private_key 57 | elif isinstance(private_key, bytes): 58 | key_value = f"0x{private_key.hex()}" 59 | else: 60 | raise TypeError("Input value must be a string or bytes.") 61 | 62 | return f"{aip80_prefix}{key_value}" 63 | 64 | @staticmethod 65 | def parse_hex_input( 66 | value: str | bytes, key_type: PrivateKeyVariant, strict: bool | None = None 67 | ) -> bytes: 68 | """ 69 | Parse a HexInput that may be a hex string, bytes, or an AIP-80 compliant string to a byte array. 70 | 71 | :param value: A hex string, byte array, or AIP-80 compliant string. 72 | :param key_type: The private key type. 73 | :param strict: If true, the value MUST be compliant with AIP-80. 74 | :return: Parsed private key as bytes. 75 | """ 76 | if key_type not in PrivateKey.AIP80_PREFIXES: 77 | raise ValueError(f"Unknown private key type: {key_type}") 78 | aip80_prefix = PrivateKey.AIP80_PREFIXES[key_type] 79 | 80 | if isinstance(value, str): 81 | if not strict and not value.startswith(aip80_prefix): 82 | # Non-AIP-80 compliant hex string 83 | if strict is None: 84 | print( 85 | "It is recommended that private keys are AIP-80 compliant (https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md)." 86 | ) 87 | if value[0:2] == "0x": 88 | value = value[2:] 89 | return bytes.fromhex(value) 90 | elif value.startswith(aip80_prefix): 91 | # AIP-80 compliant string 92 | value = value.split("-")[2] 93 | if value[0:2] == "0x": 94 | value = value[2:] 95 | return bytes.fromhex(value) 96 | else: 97 | if strict: 98 | raise ValueError( 99 | "Invalid HexString input. Must be AIP-80 compliant string." 100 | ) 101 | raise ValueError("Invalid HexString input.") 102 | elif isinstance(value, bytes): 103 | return value 104 | else: 105 | raise TypeError("Input value must be a string or bytes.") 106 | 107 | 108 | class PublicKey(Deserializable, Serializable, Protocol): 109 | def to_crypto_bytes(self) -> bytes: 110 | """ 111 | A long time ago, someone decided that we should have both bcs and a special representation 112 | for MultiEd25519, so we use this to let keys self-define a special encoding. 113 | """ 114 | ... 115 | 116 | def verify(self, data: bytes, signature: Signature) -> bool: ... 117 | 118 | 119 | class Signature(Deserializable, Serializable, Protocol): ... 120 | -------------------------------------------------------------------------------- /aptos_sdk/asymmetric_crypto_wrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | from typing import List, Tuple, cast 7 | 8 | from . import asymmetric_crypto, ed25519, secp256k1_ecdsa 9 | from .bcs import Deserializer, Serializer 10 | 11 | 12 | class PublicKey(asymmetric_crypto.PublicKey): 13 | ED25519: int = 0 14 | SECP256K1_ECDSA: int = 1 15 | 16 | variant: int 17 | public_key: asymmetric_crypto.PublicKey 18 | 19 | def __init__(self, public_key: asymmetric_crypto.PublicKey): 20 | if isinstance(public_key, ed25519.PublicKey): 21 | self.variant = PublicKey.ED25519 22 | elif isinstance(public_key, secp256k1_ecdsa.PublicKey): 23 | self.variant = PublicKey.SECP256K1_ECDSA 24 | else: 25 | raise NotImplementedError() 26 | self.public_key = public_key 27 | 28 | def to_crypto_bytes(self) -> bytes: 29 | ser = Serializer() 30 | self.serialize(ser) 31 | return ser.output() 32 | 33 | def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: 34 | # Convert signature to the original signature 35 | sig = cast(Signature, signature) 36 | 37 | return self.public_key.verify(data, sig.signature) 38 | 39 | @staticmethod 40 | def deserialize(deserializer: Deserializer) -> PublicKey: 41 | variant = deserializer.uleb128() 42 | 43 | if variant == PublicKey.ED25519: 44 | public_key: asymmetric_crypto.PublicKey = ed25519.PublicKey.deserialize( 45 | deserializer 46 | ) 47 | elif variant == Signature.SECP256K1_ECDSA: 48 | public_key = secp256k1_ecdsa.PublicKey.deserialize(deserializer) 49 | else: 50 | raise Exception(f"Invalid type: {variant}") 51 | 52 | return PublicKey(public_key) 53 | 54 | def serialize(self, serializer: Serializer): 55 | serializer.uleb128(self.variant) 56 | serializer.struct(self.public_key) 57 | 58 | 59 | class Signature(asymmetric_crypto.Signature): 60 | ED25519: int = 0 61 | SECP256K1_ECDSA: int = 1 62 | 63 | variant: int 64 | signature: asymmetric_crypto.Signature 65 | 66 | def __init__(self, signature: asymmetric_crypto.Signature): 67 | if isinstance(signature, ed25519.Signature): 68 | self.variant = Signature.ED25519 69 | elif isinstance(signature, secp256k1_ecdsa.Signature): 70 | self.variant = Signature.SECP256K1_ECDSA 71 | else: 72 | raise NotImplementedError() 73 | self.signature = signature 74 | 75 | @staticmethod 76 | def deserialize(deserializer: Deserializer) -> Signature: 77 | variant = deserializer.uleb128() 78 | 79 | if variant == Signature.ED25519: 80 | signature: asymmetric_crypto.Signature = ed25519.Signature.deserialize( 81 | deserializer 82 | ) 83 | elif variant == Signature.SECP256K1_ECDSA: 84 | signature = secp256k1_ecdsa.Signature.deserialize(deserializer) 85 | else: 86 | raise Exception(f"Invalid type: {variant}") 87 | 88 | return Signature(signature) 89 | 90 | def serialize(self, serializer: Serializer): 91 | serializer.uleb128(self.variant) 92 | serializer.struct(self.signature) 93 | 94 | 95 | class MultiPublicKey(asymmetric_crypto.PublicKey): 96 | keys: List[PublicKey] 97 | threshold: int 98 | 99 | MIN_KEYS = 2 100 | MAX_KEYS = 32 101 | MIN_THRESHOLD = 1 102 | 103 | def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int): 104 | assert ( 105 | self.MIN_KEYS <= len(keys) <= self.MAX_KEYS 106 | ), f"Must have between {self.MIN_KEYS} and {self.MAX_KEYS} keys." 107 | assert ( 108 | self.MIN_THRESHOLD <= threshold <= len(keys) 109 | ), f"Threshold must be between {self.MIN_THRESHOLD} and {len(keys)}." 110 | 111 | # Ensure keys are wrapped 112 | self.keys = [] 113 | for key in keys: 114 | if isinstance(key, PublicKey): 115 | self.keys.append(key) 116 | else: 117 | self.keys.append(PublicKey(key)) 118 | 119 | self.threshold = threshold 120 | 121 | def __str__(self) -> str: 122 | return f"{self.threshold}-of-{len(self.keys)} Multi key" 123 | 124 | def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: 125 | try: 126 | total_sig = cast(MultiSignature, signature) 127 | assert self.threshold <= len( 128 | total_sig.signatures 129 | ), f"Insufficient signatures, {self.threshold} > {len(total_sig.signatures)}" 130 | 131 | for idx, signature in total_sig.signatures: 132 | assert ( 133 | len(self.keys) > idx 134 | ), f"Signature index exceeds available keys {len(self.keys)} < {idx}" 135 | assert self.keys[idx].verify( 136 | data, signature 137 | ), "Unable to verify signature" 138 | 139 | except Exception: 140 | return False 141 | return True 142 | 143 | @staticmethod 144 | def from_crypto_bytes(indata: bytes) -> MultiPublicKey: 145 | deserializer = Deserializer(indata) 146 | return deserializer.struct(MultiPublicKey) 147 | 148 | def to_crypto_bytes(self) -> bytes: 149 | serializer = Serializer() 150 | serializer.struct(self) 151 | return serializer.output() 152 | 153 | @staticmethod 154 | def deserialize(deserializer: Deserializer) -> MultiPublicKey: 155 | keys = deserializer.sequence(PublicKey.deserialize) 156 | threshold = deserializer.u8() 157 | return MultiPublicKey(keys, threshold) 158 | 159 | def serialize(self, serializer: Serializer): 160 | serializer.sequence(self.keys, Serializer.struct) 161 | serializer.u8(self.threshold) 162 | 163 | 164 | class MultiSignature(asymmetric_crypto.Signature): 165 | signatures: List[Tuple[int, Signature]] 166 | MAX_SIGNATURES: int = 16 167 | 168 | def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]): 169 | # Sort first to ensure no issues in order 170 | # signatures.sort(key=lambda x: x[0]) 171 | self.signatures = [] 172 | for index, signature in signatures: 173 | assert index < self.MAX_SIGNATURES, "bitmap value exceeds maximum value" 174 | if isinstance(signature, Signature): 175 | self.signatures.append((index, signature)) 176 | else: 177 | self.signatures.append((index, Signature(signature))) 178 | 179 | def __eq__(self, other: object): 180 | if not isinstance(other, MultiSignature): 181 | return NotImplemented 182 | return self.signatures == other.signatures 183 | 184 | def __str__(self) -> str: 185 | return f"{self.signatures}" 186 | 187 | @staticmethod 188 | def deserialize(deserializer: Deserializer) -> MultiSignature: 189 | signatures = deserializer.sequence(Signature.deserialize) 190 | bitmap_raw = deserializer.to_bytes() 191 | bitmap = int.from_bytes(bitmap_raw, "little") 192 | num_bits = len(bitmap_raw) * 8 193 | sig_index = 0 194 | indexed_signatures = [] 195 | 196 | for i in range(0, num_bits): 197 | has_signature = (bitmap & index_to_bitmap_value(i)) != 0 198 | if has_signature: 199 | indexed_signatures.append((i, signatures[sig_index])) 200 | sig_index += 1 201 | 202 | return MultiSignature(indexed_signatures) 203 | 204 | def serialize(self, serializer: Serializer): 205 | actual_sigs = [] 206 | bitmap = 0 207 | 208 | for i, signature in self.signatures: 209 | bitmap |= index_to_bitmap_value(i) 210 | actual_sigs.append(signature) 211 | 212 | serializer.sequence(actual_sigs, Serializer.struct) 213 | count = 1 if bitmap < 256 else 2 214 | serializer.to_bytes(bitmap.to_bytes(count, "little")) 215 | 216 | 217 | def index_to_bitmap_value(i: int) -> int: 218 | bit = i % 8 219 | byte = i // 8 220 | return (128 >> bit) << (byte * 8) 221 | -------------------------------------------------------------------------------- /aptos_sdk/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from __future__ import annotations 5 | 6 | import argparse 7 | import asyncio 8 | import sys 9 | from typing import Dict, List, Tuple 10 | 11 | from .account import Account 12 | from .account_address import AccountAddress 13 | from .aptos_cli_wrapper import AptosCLIWrapper 14 | from .async_client import RestClient 15 | from .ed25519 import PrivateKey 16 | from .package_publisher import PackagePublisher 17 | 18 | 19 | async def publish_package( 20 | package_dir: str, 21 | named_addresses: Dict[str, AccountAddress], 22 | signer: Account, 23 | rest_api: str, 24 | ): 25 | AptosCLIWrapper.compile_package(package_dir, named_addresses) 26 | 27 | rest_client = RestClient(rest_api) 28 | publisher = PackagePublisher(rest_client) 29 | await publisher.publish_package_in_path(signer, package_dir) 30 | 31 | 32 | def key_value(indata: str) -> Tuple[str, AccountAddress]: 33 | split_indata = indata.split("=") 34 | if len(split_indata) != 2: 35 | raise ValueError("Invalid named-address, expected name=account address") 36 | name = split_indata[0] 37 | account_address = AccountAddress.from_str(split_indata[1]) 38 | return (name, account_address) 39 | 40 | 41 | async def main(args: List[str]): 42 | parser = argparse.ArgumentParser(description="Aptos Pyton CLI") 43 | parser.add_argument( 44 | "command", type=str, help="The command to execute", choices=["publish-package"] 45 | ) 46 | parser.add_argument( 47 | "--account", 48 | help="The account to query or the signer of a transaction", 49 | type=AccountAddress.from_str, 50 | ) 51 | parser.add_argument( 52 | "--named-address", 53 | help="A single literal address name paired to an account address, e.g., name=0x1", 54 | nargs="*", 55 | type=key_value, 56 | ) 57 | parser.add_argument("--package-dir", help="The path to the Move package", type=str) 58 | parser.add_argument( 59 | "--private-key-path", help="The path to the signer's private key", type=str 60 | ) 61 | parser.add_argument( 62 | "--rest-api", 63 | help="The REST API to send queries to, e.g., https://testnet.aptoslabs.com/v1", 64 | type=str, 65 | ) 66 | parsed_args = parser.parse_args(args) 67 | 68 | if parsed_args.command == "publish-package": 69 | if parsed_args.account is None: 70 | parser.error("Missing required argument '--account'") 71 | if parsed_args.package_dir is None: 72 | parser.error("Missing required argument '--package-dir'") 73 | if parsed_args.rest_api is None: 74 | parser.error("Missing required argument '--rest-api'") 75 | 76 | if not AptosCLIWrapper.does_cli_exist(): 77 | parser.error( 78 | "Missing Aptos CLI. Export its path to APTOS_CLI_PATH environmental variable." 79 | ) 80 | 81 | if parsed_args.private_key_path is None: 82 | parser.error("Missing required argument '--private-key-path'") 83 | with open(parsed_args.private_key_path) as f: 84 | private_key = PrivateKey.from_str(f.read()) 85 | 86 | account = Account(parsed_args.account, private_key) 87 | await publish_package( 88 | parsed_args.package_dir, 89 | dict(parsed_args.named_address), 90 | account, 91 | parsed_args.rest_api, 92 | ) 93 | 94 | 95 | if __name__ == "__main__": 96 | asyncio.run(main(sys.argv[1:])) 97 | -------------------------------------------------------------------------------- /aptos_sdk/metadata.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata as metadata 2 | 3 | # constants 4 | PACKAGE_NAME = "aptos-sdk" 5 | 6 | 7 | class Metadata: 8 | APTOS_HEADER = "x-aptos-client" 9 | 10 | @staticmethod 11 | def get_aptos_header_val(): 12 | version = metadata.version(PACKAGE_NAME) 13 | return f"aptos-python-sdk/{version}" 14 | -------------------------------------------------------------------------------- /aptos_sdk/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/aptos-python-sdk/f0da7066f1d48f81f695b62a49a707a8a4897323/aptos_sdk/py.typed -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /aptos_sdk 2 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/aptos-python-sdk/f0da7066f1d48f81f695b62a49a707a8a4897323/examples/__init__.py -------------------------------------------------------------------------------- /examples/aptos_token.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk.account import Account 7 | from aptos_sdk.account_address import AccountAddress 8 | from aptos_sdk.aptos_token_client import AptosTokenClient, Object, Property, PropertyMap 9 | from aptos_sdk.async_client import FaucetClient, RestClient 10 | 11 | from .common import FAUCET_URL, NODE_URL 12 | 13 | 14 | async def main(): 15 | rest_client = RestClient(NODE_URL) 16 | faucet_client = FaucetClient(FAUCET_URL, rest_client) 17 | token_client = AptosTokenClient(rest_client) 18 | alice = Account.generate() 19 | bob = Account.generate() 20 | 21 | collection_name = "Alice's" 22 | token_name = "Alice's first token" 23 | 24 | print("\n=== Addresses ===") 25 | print(f"Alice: {alice.address()}") 26 | print(f"Bob: {bob.address()}") 27 | 28 | bob_fund = faucet_client.fund_account(alice.address(), 100_000_000) 29 | alice_fund = faucet_client.fund_account(bob.address(), 100_000_000) 30 | await asyncio.gather(*[bob_fund, alice_fund]) 31 | 32 | print("\n=== Initial Coin Balances ===") 33 | alice_balance = rest_client.account_balance(alice.address()) 34 | bob_balance = rest_client.account_balance(bob.address()) 35 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 36 | print(f"Alice: {alice_balance}") 37 | print(f"Bob: {bob_balance}") 38 | 39 | print("\n=== Creating Collection and Token ===") 40 | 41 | txn_hash = await token_client.create_collection( 42 | alice, 43 | "Alice's simple collection", 44 | 1, 45 | collection_name, 46 | "https://aptos.dev", 47 | True, 48 | True, 49 | True, 50 | True, 51 | True, 52 | True, 53 | True, 54 | True, 55 | True, 56 | 0, 57 | 1, 58 | ) 59 | await rest_client.wait_for_transaction(txn_hash) 60 | 61 | # This is a hack, once we add support for reading events or indexer, this will be easier 62 | resp = await rest_client.account_resource(alice.address(), "0x1::account::Account") 63 | int(resp["data"]["guid_creation_num"]) 64 | 65 | txn_hash = await token_client.mint_token( 66 | alice, 67 | collection_name, 68 | "Alice's simple token", 69 | token_name, 70 | "https://aptos.dev/img/nyan.jpeg", 71 | PropertyMap([Property.string("string", "string value")]), 72 | ) 73 | await rest_client.wait_for_transaction(txn_hash) 74 | 75 | minted_tokens = await token_client.tokens_minted_from_transaction(txn_hash) 76 | assert len(minted_tokens) == 1 77 | token_addr = minted_tokens[0] 78 | 79 | collection_addr = AccountAddress.for_named_collection( 80 | alice.address(), collection_name 81 | ) 82 | collection_data = await token_client.read_object(collection_addr) 83 | print(f"Alice's collection: {collection_data}") 84 | token_data = await token_client.read_object(token_addr) 85 | print(f"Alice's token: {token_data}") 86 | 87 | txn_hash = await token_client.add_token_property( 88 | alice, token_addr, Property.bool("test", False) 89 | ) 90 | await rest_client.wait_for_transaction(txn_hash) 91 | token_data = await token_client.read_object(token_addr) 92 | print(f"Alice's token: {token_data}") 93 | txn_hash = await token_client.remove_token_property(alice, token_addr, "string") 94 | await rest_client.wait_for_transaction(txn_hash) 95 | token_data = await token_client.read_object(token_addr) 96 | print(f"Alice's token: {token_data}") 97 | txn_hash = await token_client.update_token_property( 98 | alice, token_addr, Property.bool("test", True) 99 | ) 100 | await rest_client.wait_for_transaction(txn_hash) 101 | token_data = await token_client.read_object(token_addr) 102 | print(f"Alice's token: {token_data}") 103 | txn_hash = await token_client.add_token_property( 104 | alice, token_addr, Property.bytes("bytes", b"\x00\x01") 105 | ) 106 | await rest_client.wait_for_transaction(txn_hash) 107 | token_data = await token_client.read_object(token_addr) 108 | print(f"Alice's token: {token_data}") 109 | 110 | print("\n=== Transferring the Token from Alice to Bob ===") 111 | print(f"Alice: {alice.address()}") 112 | print(f"Bob: {bob.address()}") 113 | print(f"Token: {token_addr}\n") 114 | print(f"Owner: {token_data.resources[Object].owner}") 115 | print(" ...transferring... ") 116 | txn_hash = await rest_client.transfer_object(alice, token_addr, bob.address()) 117 | await rest_client.wait_for_transaction(txn_hash) 118 | token_data = await token_client.read_object(token_addr) 119 | print(f"Owner: {token_data.resources[Object].owner}\n") 120 | 121 | await rest_client.close() 122 | 123 | 124 | if __name__ == "__main__": 125 | asyncio.run(main()) 126 | -------------------------------------------------------------------------------- /examples/common.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | import os.path 6 | 7 | APTOS_CORE_PATH = os.getenv( 8 | "APTOS_CORE_PATH", 9 | os.path.abspath("./aptos-core"), 10 | ) 11 | # :!:>section_1 12 | FAUCET_URL = os.getenv( 13 | "APTOS_FAUCET_URL", 14 | "https://faucet.devnet.aptoslabs.com", 15 | ) 16 | FAUCET_AUTH_TOKEN = os.getenv("FAUCET_AUTH_TOKEN") 17 | INDEXER_URL = os.getenv( 18 | "APTOS_INDEXER_URL", 19 | "https://api.devnet.aptoslabs.com/v1/graphql", 20 | ) 21 | NODE_URL = os.getenv("APTOS_NODE_URL", "https://api.devnet.aptoslabs.com/v1") 22 | # <:!:section_1 23 | -------------------------------------------------------------------------------- /examples/fee_payer_transfer_coin.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk.account import Account 7 | from aptos_sdk.async_client import FaucetClient, RestClient 8 | from aptos_sdk.authenticator import Authenticator, FeePayerAuthenticator 9 | from aptos_sdk.bcs import Serializer 10 | from aptos_sdk.transactions import ( 11 | EntryFunction, 12 | FeePayerRawTransaction, 13 | SignedTransaction, 14 | TransactionArgument, 15 | TransactionPayload, 16 | ) 17 | 18 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 19 | 20 | 21 | async def main(): 22 | # :!:>section_1 23 | rest_client = RestClient(NODE_URL) 24 | faucet_client = FaucetClient( 25 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 26 | ) # <:!:section_1 27 | 28 | # :!:>section_2 29 | alice = Account.generate() 30 | bob = Account.generate() 31 | sponsor = Account.generate() # <:!:section_2 32 | 33 | print("\n=== Addresses ===") 34 | print(f"Alice: {alice.address()}") 35 | print(f"Bob: {bob.address()}") 36 | print(f"Sponsor: {sponsor.address()}") 37 | 38 | # :!:>section_3 39 | await faucet_client.fund_account(sponsor.address(), 100_000_000) # <:!:section_3 40 | 41 | print("\n=== Initial Data ===") 42 | # :!:>section_4 43 | alice_sequence_number = await rest_client.account_sequence_number(alice.address()) 44 | bob_balance = await rest_client.account_balance(bob.address()) 45 | sponsor_balance = await rest_client.account_balance(sponsor.address()) 46 | print(f"Alice sequence number: {alice_sequence_number}") 47 | print(f"Bob balance: {bob_balance}") 48 | print(f"Sponsor balance: {sponsor_balance}") # <:!:section_4 49 | 50 | # Have Alice give Bob 1_000 coins via a sponsored transaction 51 | # :!:>section_5 52 | transaction_arguments = [ 53 | TransactionArgument(bob.address(), Serializer.struct), 54 | ] 55 | 56 | payload = EntryFunction.natural( 57 | "0x1::aptos_account", 58 | "create_account", 59 | [], 60 | transaction_arguments, 61 | ) 62 | raw_transaction = await rest_client.create_bcs_transaction( 63 | alice, TransactionPayload(payload), alice_sequence_number 64 | ) 65 | fee_payer_transaction = FeePayerRawTransaction(raw_transaction, [], None) 66 | sender_authenticator = alice.sign_transaction(fee_payer_transaction) 67 | fee_payer_transaction = FeePayerRawTransaction( 68 | raw_transaction, [], sponsor.address() 69 | ) 70 | sponsor_authenticator = sponsor.sign_transaction(fee_payer_transaction) 71 | fee_payer_authenticator = FeePayerAuthenticator( 72 | sender_authenticator, [], (sponsor.address(), sponsor_authenticator) 73 | ) 74 | signed_transaction = SignedTransaction( 75 | raw_transaction, Authenticator(fee_payer_authenticator) 76 | ) 77 | txn_hash = await rest_client.submit_bcs_transaction( 78 | signed_transaction 79 | ) # <:!:section_5 80 | # :!:>section_6 81 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_6 82 | 83 | print("\n=== Final Data ===") 84 | alice_sequence_number = rest_client.account_sequence_number(alice.address()) 85 | bob_balance = rest_client.account_balance(bob.address()) 86 | sponsor_balance = rest_client.account_balance(sponsor.address()) 87 | [alice_sequence_number, bob_balance, sponsor_balance] = await asyncio.gather( 88 | *[alice_sequence_number, bob_balance, sponsor_balance] 89 | ) 90 | print(f"Alice sequence number: {alice_sequence_number}") 91 | print(f"Bob balance: {bob_balance}") 92 | print(f"Sponsor balance: {sponsor_balance}") # <:!:section_4 93 | 94 | await rest_client.close() 95 | 96 | 97 | if __name__ == "__main__": 98 | asyncio.run(main()) 99 | -------------------------------------------------------------------------------- /examples/hello_blockchain.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This example depends on the hello_blockchain.move module having already been published to the destination blockchain. 6 | 7 | One method to do so is to use the CLI: 8 | * Acquire the Aptos CLI 9 | * `cd ~` 10 | * `aptos init` 11 | * `cd ~/aptos-core/aptos-move/move-examples/hello_blockchain` 12 | * `aptos move publish --named-addresses hello_blockchain=${your_address_from_aptos_init}` 13 | * `python -m examples.hello-blockchain ${your_address_from_aptos_init}` 14 | """ 15 | 16 | import asyncio 17 | import os 18 | import sys 19 | from typing import Any, Dict, Optional 20 | 21 | from aptos_sdk.account import Account 22 | from aptos_sdk.account_address import AccountAddress 23 | from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper 24 | from aptos_sdk.async_client import FaucetClient, ResourceNotFound, RestClient 25 | from aptos_sdk.bcs import Serializer 26 | from aptos_sdk.package_publisher import PackagePublisher 27 | from aptos_sdk.transactions import ( 28 | EntryFunction, 29 | TransactionArgument, 30 | TransactionPayload, 31 | ) 32 | 33 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 34 | 35 | 36 | class HelloBlockchainClient(RestClient): 37 | async def get_message( 38 | self, contract_address: AccountAddress, account_address: AccountAddress 39 | ) -> Optional[Dict[str, Any]]: 40 | """Retrieve the resource message::MessageHolder::message""" 41 | try: 42 | return await self.account_resource( 43 | account_address, f"{contract_address}::message::MessageHolder" 44 | ) 45 | except ResourceNotFound: 46 | return None 47 | 48 | async def set_message( 49 | self, contract_address: AccountAddress, sender: Account, message: str 50 | ) -> str: 51 | """Potentially initialize and set the resource message::MessageHolder::message""" 52 | 53 | payload = EntryFunction.natural( 54 | f"{contract_address}::message", 55 | "set_message", 56 | [], 57 | [TransactionArgument(message, Serializer.str)], 58 | ) 59 | signed_transaction = await self.create_bcs_signed_transaction( 60 | sender, TransactionPayload(payload) 61 | ) 62 | return await self.submit_bcs_transaction(signed_transaction) 63 | 64 | 65 | async def publish_contract(package_dir: str) -> AccountAddress: 66 | contract_publisher = Account.generate() 67 | rest_client = HelloBlockchainClient(NODE_URL) 68 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 69 | await faucet_client.fund_account(contract_publisher.address(), 10_000_000) 70 | 71 | AptosCLIWrapper.compile_package( 72 | package_dir, {"hello_blockchain": contract_publisher.address()} 73 | ) 74 | 75 | module_path = os.path.join( 76 | package_dir, "build", "Examples", "bytecode_modules", "message.mv" 77 | ) 78 | with open(module_path, "rb") as f: 79 | module = f.read() 80 | 81 | metadata_path = os.path.join( 82 | package_dir, "build", "Examples", "package-metadata.bcs" 83 | ) 84 | with open(metadata_path, "rb") as f: 85 | metadata = f.read() 86 | 87 | package_publisher = PackagePublisher(rest_client) 88 | txn_hash = await package_publisher.publish_package( 89 | contract_publisher, metadata, [module] 90 | ) 91 | await rest_client.wait_for_transaction(txn_hash) 92 | 93 | await rest_client.close() 94 | 95 | return contract_publisher.address() 96 | 97 | 98 | async def main(contract_address: AccountAddress): 99 | alice = Account.generate() 100 | bob = Account.generate() 101 | 102 | print("\n=== Addresses ===") 103 | print(f"Alice: {alice.address()}") 104 | print(f"Bob: {bob.address()}") 105 | 106 | rest_client = HelloBlockchainClient(NODE_URL) 107 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 108 | 109 | alice_fund = faucet_client.fund_account(alice.address(), 10_000_000) 110 | bob_fund = faucet_client.fund_account(bob.address(), 10_000_000) 111 | await asyncio.gather(*[alice_fund, bob_fund]) 112 | 113 | a_alice_balance = rest_client.account_balance(alice.address()) 114 | a_bob_balance = rest_client.account_balance(bob.address()) 115 | [alice_balance, bob_balance] = await asyncio.gather( 116 | *[a_alice_balance, a_bob_balance] 117 | ) 118 | 119 | print("\n=== Initial Balances ===") 120 | print(f"Alice: {alice_balance}") 121 | print(f"Bob: {bob_balance}") 122 | 123 | print("\n=== Testing Alice ===") 124 | message = await rest_client.get_message(contract_address, alice.address()) 125 | print(f"Initial value: {message}") 126 | print('Setting the message to "Hello, Blockchain"') 127 | txn_hash = await rest_client.set_message( 128 | contract_address, alice, "Hello, Blockchain" 129 | ) 130 | await rest_client.wait_for_transaction(txn_hash) 131 | 132 | message = await rest_client.get_message(contract_address, alice.address()) 133 | print(f"New value: {message}") 134 | 135 | print("\n=== Testing Bob ===") 136 | message = await rest_client.get_message(contract_address, bob.address()) 137 | print(f"Initial value: {message}") 138 | print('Setting the message to "Hello, Blockchain"') 139 | txn_hash = await rest_client.set_message(contract_address, bob, "Hello, Blockchain") 140 | await rest_client.wait_for_transaction(txn_hash) 141 | 142 | message = await rest_client.get_message(contract_address, bob.address()) 143 | print(f"New value: {message}") 144 | 145 | await rest_client.close() 146 | 147 | 148 | if __name__ == "__main__": 149 | assert len(sys.argv) == 2, "Expecting the contract address" 150 | contract_address = sys.argv[1] 151 | 152 | asyncio.run(main(AccountAddress.from_str(contract_address))) 153 | -------------------------------------------------------------------------------- /examples/integration_test.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | Provides a test harness for treating examples as integration tests. 6 | """ 7 | 8 | import asyncio 9 | import os 10 | import unittest 11 | from typing import Optional 12 | 13 | from aptos_sdk.account_address import AccountAddress 14 | from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper, AptosInstance 15 | 16 | from .common import APTOS_CORE_PATH 17 | 18 | 19 | class Test(unittest.IsolatedAsyncioTestCase): 20 | _node: Optional[AptosInstance] = None 21 | 22 | @classmethod 23 | def setUpClass(self): 24 | if os.getenv("APTOS_TEST_USE_EXISTING_NETWORK"): 25 | return 26 | 27 | self._node = AptosCLIWrapper.start_node() 28 | operational = asyncio.run(self._node.wait_until_operational()) 29 | if not operational: 30 | raise Exception("".join(self._node.errors())) 31 | 32 | os.environ["APTOS_FAUCET_URL"] = "http://127.0.0.1:8081" 33 | os.environ["APTOS_INDEXER_CLIENT"] = "none" 34 | os.environ["APTOS_NODE_URL"] = "http://127.0.0.1:8080/v1" 35 | 36 | async def test_aptos_token(self): 37 | return 38 | from . import aptos_token 39 | 40 | await aptos_token.main() 41 | 42 | async def test_fee_payer_transfer_coin(self): 43 | from . import fee_payer_transfer_coin 44 | 45 | await fee_payer_transfer_coin.main() 46 | 47 | async def test_hello_blockchain(self): 48 | from . import hello_blockchain 49 | 50 | hello_blockchain_dir = os.path.join( 51 | APTOS_CORE_PATH, "aptos-move", "move-examples", "hello_blockchain" 52 | ) 53 | AptosCLIWrapper.test_package( 54 | hello_blockchain_dir, {"hello_blockchain": AccountAddress.from_str("0xa")} 55 | ) 56 | contract_address = await hello_blockchain.publish_contract(hello_blockchain_dir) 57 | await hello_blockchain.main(contract_address) 58 | 59 | async def test_large_package_publisher(self): 60 | # TODO -- this is currently broken, out of gas 61 | return 62 | 63 | from . import large_package_publisher 64 | 65 | large_packages_dir = os.path.join( 66 | APTOS_CORE_PATH, "aptos-move", "move-examples", "large_packages" 67 | ) 68 | module_addr = await large_package_publisher.publish_large_packages( 69 | large_packages_dir 70 | ) 71 | large_package_example_dir = os.path.join( 72 | large_packages_dir, "large_package_example" 73 | ) 74 | await large_package_publisher.main(large_package_example_dir, module_addr) 75 | 76 | async def test_multikey(self): 77 | from . import multikey 78 | 79 | await multikey.main() 80 | 81 | async def test_multisig(self): 82 | from . import multisig 83 | 84 | # This test is currently broken, needs an aptos core checkout 85 | return 86 | await multisig.main(False) 87 | 88 | async def test_read_aggreagtor(self): 89 | from . import read_aggregator 90 | 91 | await read_aggregator.main() 92 | 93 | async def test_rotate_key(self): 94 | from . import rotate_key 95 | 96 | await rotate_key.main() 97 | 98 | async def test_secp256k1_ecdsa_transfer_coin(self): 99 | from . import secp256k1_ecdsa_transfer_coin 100 | 101 | await secp256k1_ecdsa_transfer_coin.main() 102 | 103 | async def test_simple_aptos_token(self): 104 | from . import simple_aptos_token 105 | 106 | await simple_aptos_token.main() 107 | 108 | async def test_simple_nft(self): 109 | from . import simple_nft 110 | 111 | await simple_nft.main() 112 | 113 | async def test_simulate_transfer_coin(self): 114 | from . import simulate_transfer_coin 115 | 116 | await simulate_transfer_coin.main() 117 | 118 | async def test_transfer_coin(self): 119 | from . import transfer_coin 120 | 121 | await transfer_coin.main() 122 | 123 | async def test_transfer_two_by_two(self): 124 | from . import transfer_two_by_two 125 | 126 | await transfer_two_by_two.main() 127 | 128 | async def test_your_coin(self): 129 | from . import your_coin 130 | 131 | moon_coin_path = os.path.join( 132 | APTOS_CORE_PATH, "aptos-move", "move-examples", "moon_coin" 133 | ) 134 | AptosCLIWrapper.test_package( 135 | moon_coin_path, {"MoonCoin": AccountAddress.from_str("0xa")} 136 | ) 137 | await your_coin.main(moon_coin_path) 138 | 139 | @classmethod 140 | def tearDownClass(self): 141 | if os.getenv("APTOS_TEST_USE_EXISTING_NETWORK"): 142 | return 143 | 144 | self._node.stop() 145 | 146 | 147 | if __name__ == "__main__": 148 | unittest.main(buffer=True) 149 | -------------------------------------------------------------------------------- /examples/large_package_publisher.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This example depends on the MoonCoin.move module having already been published to the destination blockchain. 5 | One method to do so is to use the CLI: 6 | * Acquire the Aptos CLI, see https://aptos.dev/cli-tools/aptos-cli-tool/install-aptos-cli 7 | * `python -m examples.your-coin ~/aptos-core/aptos-move/move-examples/moon_coin`. 8 | * Open another terminal and `aptos move compile --package-dir ~/aptos-core/aptos-move/move-examples/moon_coin --save-metadata --named-addresses MoonCoin=`. 9 | * Return to the first terminal and press enter. 10 | """ 11 | import asyncio 12 | import os 13 | import sys 14 | 15 | import aptos_sdk.cli as aptos_sdk_cli 16 | from aptos_sdk.account import Account 17 | from aptos_sdk.account_address import AccountAddress 18 | from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper 19 | from aptos_sdk.async_client import ClientConfig, FaucetClient, RestClient 20 | from aptos_sdk.package_publisher import MODULE_ADDRESS, PackagePublisher 21 | 22 | from .common import APTOS_CORE_PATH, FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 23 | 24 | 25 | async def publish_large_packages(large_packages_dir) -> AccountAddress: 26 | rest_client = RestClient(NODE_URL) 27 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 28 | 29 | alice = Account.generate() 30 | await faucet_client.fund_account(alice.address(), 1_000_000_000) 31 | await aptos_sdk_cli.publish_package( 32 | large_packages_dir, {"large_packages": alice.address()}, alice, NODE_URL 33 | ) 34 | return alice.address() 35 | 36 | 37 | async def main( 38 | large_package_example_dir, 39 | large_packages_account: AccountAddress = MODULE_ADDRESS, 40 | ): 41 | client_config = ClientConfig() 42 | client_config.transaction_wait_in_seconds = 120 43 | client_config.max_gas_amount = 1_000_000 44 | rest_client = RestClient(NODE_URL, client_config) 45 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 46 | 47 | alice = Account.generate() 48 | req0 = faucet_client.fund_account(alice.address(), 1_000_000_000) 49 | req1 = faucet_client.fund_account(alice.address(), 1_000_000_000) 50 | req2 = faucet_client.fund_account(alice.address(), 1_000_000_000) 51 | await asyncio.gather(*[req0, req1, req2]) 52 | alice_balance = await rest_client.account_balance(alice.address()) 53 | print(f"Alice: {alice.address()} {alice_balance}") 54 | 55 | if AptosCLIWrapper.does_cli_exist(): 56 | AptosCLIWrapper.compile_package( 57 | large_package_example_dir, {"large_package_example": alice.address()} 58 | ) 59 | else: 60 | input("\nUpdate the module with Alice's address, compile, and press Enter.") 61 | 62 | publisher = PackagePublisher(rest_client) 63 | await publisher.publish_package_in_path( 64 | alice, large_package_example_dir, large_packages_account 65 | ) 66 | 67 | await rest_client.close() 68 | 69 | 70 | if __name__ == "__main__": 71 | if len(sys.argv) == 2: 72 | large_package_example_dir = sys.argv[1] 73 | else: 74 | large_package_example_dir = os.path.join( 75 | APTOS_CORE_PATH, 76 | "aptos-move", 77 | "move-examples", 78 | "large_packages", 79 | "large_package_example", 80 | ) 81 | asyncio.run(main(large_package_example_dir)) 82 | -------------------------------------------------------------------------------- /examples/multikey.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk import asymmetric_crypto_wrapper, ed25519, secp256k1_ecdsa 7 | from aptos_sdk.account import Account 8 | from aptos_sdk.account_address import AccountAddress 9 | from aptos_sdk.asymmetric_crypto_wrapper import MultiSignature, Signature 10 | from aptos_sdk.async_client import FaucetClient, IndexerClient, RestClient 11 | from aptos_sdk.authenticator import AccountAuthenticator, MultiKeyAuthenticator 12 | from aptos_sdk.bcs import Serializer 13 | from aptos_sdk.transactions import ( 14 | EntryFunction, 15 | SignedTransaction, 16 | TransactionArgument, 17 | TransactionPayload, 18 | ) 19 | 20 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, INDEXER_URL, NODE_URL 21 | 22 | 23 | async def main(): 24 | # :!:>section_1 25 | rest_client = RestClient(NODE_URL) 26 | faucet_client = FaucetClient( 27 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 28 | ) # <:!:section_1 29 | if INDEXER_URL and INDEXER_URL != "none": 30 | IndexerClient(INDEXER_URL) 31 | else: 32 | pass 33 | 34 | # :!:>section_2 35 | key1 = secp256k1_ecdsa.PrivateKey.random() 36 | key2 = ed25519.PrivateKey.random() 37 | key3 = secp256k1_ecdsa.PrivateKey.random() 38 | pubkey1 = key1.public_key() 39 | pubkey2 = key2.public_key() 40 | pubkey3 = key3.public_key() 41 | 42 | alice_pubkey = asymmetric_crypto_wrapper.MultiPublicKey( 43 | [pubkey1, pubkey2, pubkey3], 2 44 | ) 45 | alice_address = AccountAddress.from_key(alice_pubkey) 46 | 47 | bob = Account.generate() 48 | 49 | print("\n=== Addresses ===") 50 | print(f"Multikey Alice: {alice_address}") 51 | print(f"Bob: {bob.address()}") 52 | 53 | # :!:>section_3 54 | alice_fund = faucet_client.fund_account(alice_address, 100_000_000) 55 | bob_fund = faucet_client.fund_account(bob.address(), 1) # <:!:section_3 56 | await asyncio.gather(*[alice_fund, bob_fund]) 57 | 58 | print("\n=== Initial Balances ===") 59 | # :!:>section_4 60 | alice_balance = rest_client.account_balance(alice_address) 61 | bob_balance = rest_client.account_balance(bob.address()) 62 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 63 | print(f"Alice: {alice_balance}") 64 | print(f"Bob: {bob_balance}") # <:!:section_4 65 | 66 | # Have Alice give Bob 1_000 coins 67 | # :!:>section_5 68 | 69 | # TODO: Rework SDK to support this without the extra work 70 | 71 | # Build Transaction to sign 72 | transaction_arguments = [ 73 | TransactionArgument(bob.address(), Serializer.struct), 74 | TransactionArgument(1_000, Serializer.u64), 75 | ] 76 | 77 | payload = EntryFunction.natural( 78 | "0x1::aptos_account", 79 | "transfer", 80 | [], 81 | transaction_arguments, 82 | ) 83 | 84 | raw_transaction = await rest_client.create_bcs_transaction( 85 | alice_address, TransactionPayload(payload) 86 | ) 87 | 88 | # Sign by multiple keys 89 | raw_txn_bytes = raw_transaction.keyed() 90 | sig1 = key1.sign(raw_txn_bytes) 91 | sig2 = key2.sign(raw_txn_bytes) 92 | 93 | # Combine them 94 | total_sig = MultiSignature([(0, Signature(sig1)), (1, Signature(sig2))]) 95 | alice_auth = AccountAuthenticator(MultiKeyAuthenticator(alice_pubkey, total_sig)) 96 | 97 | # Verify signatures 98 | assert key1.public_key().verify(raw_txn_bytes, sig1) 99 | assert key2.public_key().verify(raw_txn_bytes, sig2) 100 | assert alice_pubkey.verify(raw_txn_bytes, total_sig) 101 | assert alice_auth.verify(raw_txn_bytes) 102 | 103 | # Submit to network 104 | signed_txn = SignedTransaction(raw_transaction, alice_auth) 105 | txn_hash = await rest_client.submit_bcs_transaction(signed_txn) 106 | 107 | # :!:>section_6 108 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_6 109 | 110 | print("\n=== Final Balances ===") 111 | alice_balance = rest_client.account_balance(alice_address) 112 | bob_balance = rest_client.account_balance(bob.address()) 113 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 114 | print(f"Alice: {alice_balance}") 115 | print(f"Bob: {bob_balance}") # <:!:section_4 116 | 117 | await rest_client.close() 118 | 119 | 120 | if __name__ == "__main__": 121 | asyncio.run(main()) 122 | -------------------------------------------------------------------------------- /examples/object_code_deployment.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | import os 6 | import sys 7 | 8 | from aptos_sdk.account import Account 9 | from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper 10 | from aptos_sdk.async_client import FaucetClient, RestClient 11 | from aptos_sdk.package_publisher import MODULE_ADDRESS, PackagePublisher, PublishMode 12 | 13 | from .common import APTOS_CORE_PATH, FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 14 | 15 | 16 | async def main(package_dir): 17 | rest_client = RestClient(NODE_URL) 18 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 19 | package_publisher = PackagePublisher(rest_client) 20 | alice = Account.generate() 21 | 22 | print("\n=== Publisher Address ===") 23 | print(f"Alice: {alice.address()}") 24 | 25 | await faucet_client.fund_account(alice.address(), 100_000_000) 26 | 27 | print("\n=== Initial Coin Balance ===") 28 | alice_balance = await rest_client.account_balance(alice.address()) 29 | print(f"Alice: {alice_balance}") 30 | 31 | # The object address is derived from publisher's address and sequence number. 32 | code_object_address = await package_publisher.derive_object_address(alice.address()) 33 | module_name = "hello_blockchain" 34 | 35 | print("\nCompiling package...") 36 | if AptosCLIWrapper.does_cli_exist(): 37 | AptosCLIWrapper.compile_package(package_dir, {module_name: code_object_address}) 38 | else: 39 | print(f"Address of the object to be created: {code_object_address}") 40 | input( 41 | "\nUpdate the module with the derived code object address, compile, and press enter." 42 | ) 43 | 44 | # Deploy package to code object. 45 | print("\n=== Object Code Deployment ===") 46 | deploy_txn_hash = await package_publisher.publish_package_in_path( 47 | alice, package_dir, MODULE_ADDRESS, publish_mode=PublishMode.OBJECT_DEPLOY 48 | ) 49 | 50 | print(f"Tx submitted: {deploy_txn_hash[0]}") 51 | await rest_client.wait_for_transaction(deploy_txn_hash[0]) 52 | print(f"Package deployed to object {code_object_address}") 53 | 54 | print("\n=== Object Code Upgrade ===") 55 | upgrade_txn_hash = await package_publisher.publish_package_in_path( 56 | alice, 57 | package_dir, 58 | MODULE_ADDRESS, 59 | publish_mode=PublishMode.OBJECT_UPGRADE, 60 | code_object=code_object_address, 61 | ) 62 | print(f"Tx submitted: {upgrade_txn_hash[0]}") 63 | await rest_client.wait_for_transaction(upgrade_txn_hash[0]) 64 | print(f"Package in object {code_object_address} upgraded") 65 | await rest_client.close() 66 | 67 | 68 | if __name__ == "__main__": 69 | if len(sys.argv) == 2: 70 | package_dir = sys.argv[1] 71 | else: 72 | package_dir = os.path.join( 73 | APTOS_CORE_PATH, 74 | "aptos-move", 75 | "move-examples", 76 | "hello_blockchain", 77 | ) 78 | 79 | asyncio.run(main(package_dir)) 80 | -------------------------------------------------------------------------------- /examples/read_aggregator.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk.account_address import AccountAddress 7 | from aptos_sdk.async_client import RestClient 8 | 9 | from .common import NODE_URL 10 | 11 | 12 | async def main(): 13 | rest_client = RestClient(NODE_URL) 14 | total_apt = await rest_client.aggregator_value( 15 | AccountAddress.from_str("0x1"), 16 | "0x1::coin::CoinInfo<0x1::aptos_coin::AptosCoin>", 17 | ["supply"], 18 | ) 19 | print(f"Total circulating APT: {total_apt}") 20 | await rest_client.close() 21 | 22 | 23 | if __name__ == "__main__": 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /examples/rotate_key.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import List, cast 3 | 4 | import aptos_sdk.asymmetric_crypto as asymmetric_crypto 5 | import aptos_sdk.ed25519 as ed25519 6 | from aptos_sdk.account import Account, RotationProofChallenge 7 | from aptos_sdk.account_address import AccountAddress 8 | from aptos_sdk.async_client import FaucetClient, RestClient 9 | from aptos_sdk.authenticator import Authenticator 10 | from aptos_sdk.bcs import Serializer 11 | from aptos_sdk.transactions import ( 12 | EntryFunction, 13 | TransactionArgument, 14 | TransactionPayload, 15 | ) 16 | 17 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 18 | 19 | WIDTH = 19 20 | 21 | 22 | def truncate(address: str) -> str: 23 | return address[0:6] + "..." + address[-6:] 24 | 25 | 26 | def format_account_info(account: Account) -> str: 27 | vals = [ 28 | str(account.address()), 29 | account.auth_key(), 30 | account.private_key.hex(), 31 | str(account.public_key()), 32 | ] 33 | return "".join([truncate(v).ljust(WIDTH, " ") for v in vals]) 34 | 35 | 36 | async def rotate_auth_key_ed_25519_payload( 37 | rest_client: RestClient, from_account: Account, private_key: ed25519.PrivateKey 38 | ) -> TransactionPayload: 39 | to_account = Account.load_key(private_key.hex()) 40 | rotation_proof_challenge = RotationProofChallenge( 41 | sequence_number=await rest_client.account_sequence_number( 42 | from_account.address() 43 | ), 44 | originator=from_account.address(), 45 | current_auth_key=AccountAddress.from_str_relaxed(from_account.auth_key()), 46 | new_public_key=to_account.public_key(), 47 | ) 48 | 49 | serializer = Serializer() 50 | rotation_proof_challenge.serialize(serializer) 51 | rotation_proof_challenge_bcs = serializer.output() 52 | 53 | from_signature = from_account.sign(rotation_proof_challenge_bcs) 54 | to_signature = to_account.sign(rotation_proof_challenge_bcs) 55 | 56 | return rotation_payload( 57 | from_account.public_key(), to_account.public_key(), from_signature, to_signature 58 | ) 59 | 60 | 61 | async def rotate_auth_key_multi_ed_25519_payload( 62 | rest_client: RestClient, 63 | from_account: Account, 64 | private_keys: List[ed25519.PrivateKey], 65 | ) -> TransactionPayload: 66 | to_accounts = list( 67 | map(lambda private_key: Account.load_key(private_key.hex()), private_keys) 68 | ) 69 | public_keys = list(map(lambda account: account.public_key(), to_accounts)) 70 | public_key = ed25519.MultiPublicKey(cast(List[ed25519.PublicKey], public_keys), 1) 71 | 72 | rotation_proof_challenge = RotationProofChallenge( 73 | sequence_number=await rest_client.account_sequence_number( 74 | from_account.address() 75 | ), 76 | originator=from_account.address(), 77 | current_auth_key=AccountAddress.from_str(from_account.auth_key()), 78 | new_public_key=public_key, 79 | ) 80 | 81 | serializer = Serializer() 82 | rotation_proof_challenge.serialize(serializer) 83 | rotation_proof_challenge_bcs = serializer.output() 84 | 85 | from_signature = from_account.sign(rotation_proof_challenge_bcs) 86 | to_signature = cast( 87 | ed25519.Signature, to_accounts[0].sign(rotation_proof_challenge_bcs) 88 | ) 89 | multi_to_signature = ed25519.MultiSignature.from_key_map( 90 | public_key, 91 | [(cast(ed25519.PublicKey, to_accounts[0].public_key()), to_signature)], 92 | ) 93 | 94 | return rotation_payload( 95 | from_account.public_key(), public_key, from_signature, multi_to_signature 96 | ) 97 | 98 | 99 | def rotation_payload( 100 | from_key: asymmetric_crypto.PublicKey, 101 | to_key: asymmetric_crypto.PublicKey, 102 | from_signature: asymmetric_crypto.Signature, 103 | to_signature: asymmetric_crypto.Signature, 104 | ) -> TransactionPayload: 105 | from_scheme = Authenticator.from_key(from_key) 106 | to_scheme = Authenticator.from_key(to_key) 107 | 108 | entry_function = EntryFunction.natural( 109 | module="0x1::account", 110 | function="rotate_authentication_key", 111 | ty_args=[], 112 | args=[ 113 | TransactionArgument(from_scheme, Serializer.u8), 114 | TransactionArgument(from_key, Serializer.struct), 115 | TransactionArgument(to_scheme, Serializer.u8), 116 | TransactionArgument(to_key, Serializer.struct), 117 | TransactionArgument(from_signature, Serializer.struct), 118 | TransactionArgument(to_signature, Serializer.struct), 119 | ], 120 | ) 121 | 122 | return TransactionPayload(entry_function) 123 | 124 | 125 | async def main(): 126 | # Initialize the clients used to interact with the blockchain 127 | rest_client = RestClient(NODE_URL) 128 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 129 | 130 | # Generate random accounts Alice and Bob 131 | alice = Account.generate() 132 | bob = Account.generate() 133 | 134 | # Fund Alice's account, since we don't use Bob's 135 | await faucet_client.fund_account(alice.address(), 100_000_000) 136 | 137 | # Display formatted account info 138 | print( 139 | "\n" 140 | + "Account".ljust(WIDTH, " ") 141 | + "Address".ljust(WIDTH, " ") 142 | + "Auth Key".ljust(WIDTH, " ") 143 | + "Private Key".ljust(WIDTH, " ") 144 | + "Public Key".ljust(WIDTH, " ") 145 | ) 146 | print( 147 | "-------------------------------------------------------------------------------------------" 148 | ) 149 | print("Alice".ljust(WIDTH, " ") + format_account_info(alice)) 150 | print("Bob".ljust(WIDTH, " ") + format_account_info(bob)) 151 | 152 | print("\n...rotating...\n") 153 | 154 | # :!:>rotate_key 155 | # Create the payload for rotating Alice's private key to Bob's private key 156 | payload = await rotate_auth_key_ed_25519_payload( 157 | rest_client, alice, bob.private_key 158 | ) 159 | # Have Alice sign the transaction with the payload 160 | signed_transaction = await rest_client.create_bcs_signed_transaction(alice, payload) 161 | # Submit the transaction and wait for confirmation 162 | tx_hash = await rest_client.submit_bcs_transaction(signed_transaction) 163 | await rest_client.wait_for_transaction(tx_hash) # <:!:rotate_key 164 | 165 | # Check the authentication key for Alice's address on-chain 166 | alice_new_account_info = await rest_client.account(alice.address()) 167 | # Ensure that Alice's authentication key matches bob's 168 | assert ( 169 | alice_new_account_info["authentication_key"] == bob.auth_key() 170 | ), "Authentication key doesn't match Bob's" 171 | 172 | # Construct a new Account object that reflects alice's original address with the new private key 173 | original_alice_key = alice.private_key 174 | alice = Account(alice.address(), bob.private_key) 175 | 176 | # Display formatted account info 177 | print("Alice".ljust(WIDTH, " ") + format_account_info(alice)) 178 | print("Bob".ljust(WIDTH, " ") + format_account_info(bob)) 179 | print() 180 | 181 | print("\n...rotating...\n") 182 | payload = await rotate_auth_key_multi_ed_25519_payload( 183 | rest_client, alice, [bob.private_key, original_alice_key] 184 | ) 185 | signed_transaction = await rest_client.create_bcs_signed_transaction(alice, payload) 186 | tx_hash = await rest_client.submit_bcs_transaction(signed_transaction) 187 | await rest_client.wait_for_transaction(tx_hash) 188 | 189 | alice_new_account_info = await rest_client.account(alice.address()) 190 | auth_key = alice_new_account_info["authentication_key"] 191 | print(f"Rotation to MultiPublicKey complete, new authkey: {auth_key}") 192 | 193 | await rest_client.close() 194 | 195 | 196 | if __name__ == "__main__": 197 | asyncio.run(main()) 198 | -------------------------------------------------------------------------------- /examples/secp256k1_ecdsa_transfer_coin.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk.account import Account 7 | from aptos_sdk.async_client import FaucetClient, RestClient 8 | 9 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 10 | 11 | 12 | async def main(): 13 | # :!:>section_1 14 | rest_client = RestClient(NODE_URL) 15 | faucet_client = FaucetClient( 16 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 17 | ) # <:!:section_1 18 | 19 | # :!:>section_2 20 | alice = Account.generate_secp256k1_ecdsa() 21 | bob = Account.generate_secp256k1_ecdsa() # <:!:section_2 22 | 23 | print("\n=== Addresses ===") 24 | print(f"Alice: {alice.address()}") 25 | print(f"Bob: {bob.address()}") 26 | 27 | # :!:>section_3 28 | alice_fund = faucet_client.fund_account(alice.address(), 100_000_000) 29 | bob_fund = faucet_client.fund_account(bob.address(), 1) # <:!:section_3 30 | await asyncio.gather(*[alice_fund, bob_fund]) 31 | 32 | print("\n=== Initial Balances ===") 33 | # :!:>section_4 34 | alice_balance = rest_client.account_balance(alice.address()) 35 | bob_balance = rest_client.account_balance(bob.address()) 36 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 37 | print(f"Alice: {alice_balance}") 38 | print(f"Bob: {bob_balance}") # <:!:section_4 39 | 40 | # Have Alice give Bob 1_000 coins 41 | # :!:>section_5 42 | txn_hash = await rest_client.bcs_transfer( 43 | alice, bob.address(), 1_000 44 | ) # <:!:section_5 45 | # :!:>section_6 46 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_6 47 | 48 | print("\n=== Intermediate Balances ===") 49 | alice_balance = rest_client.account_balance(alice.address()) 50 | bob_balance = rest_client.account_balance(bob.address()) 51 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 52 | print(f"Alice: {alice_balance}") 53 | print(f"Bob: {bob_balance}") # <:!:section_4 54 | 55 | # Have Alice give Bob another 1_000 coins using BCS 56 | txn_hash = await rest_client.bcs_transfer(alice, bob.address(), 1_000) 57 | await rest_client.wait_for_transaction(txn_hash) 58 | 59 | print("\n=== Final Balances ===") 60 | alice_balance = rest_client.account_balance(alice.address()) 61 | bob_balance = rest_client.account_balance(bob.address()) 62 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 63 | print(f"Alice: {alice_balance}") 64 | print(f"Bob: {bob_balance}") 65 | 66 | await rest_client.close() 67 | 68 | 69 | if __name__ == "__main__": 70 | asyncio.run(main()) 71 | -------------------------------------------------------------------------------- /examples/simple_aptos_token.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | import json 6 | 7 | from aptos_sdk.account import Account 8 | from aptos_sdk.account_address import AccountAddress 9 | from aptos_sdk.aptos_token_client import ( 10 | AptosTokenClient, 11 | Collection, 12 | Object, 13 | PropertyMap, 14 | ReadObject, 15 | Token, 16 | ) 17 | from aptos_sdk.async_client import FaucetClient, RestClient 18 | 19 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 20 | 21 | 22 | def get_owner(obj: ReadObject) -> AccountAddress: 23 | return obj.resources[Object].owner 24 | 25 | 26 | # :!:>section_6 27 | async def get_collection_data( 28 | token_client: AptosTokenClient, collection_addr: AccountAddress 29 | ) -> dict[str, str]: 30 | collection = (await token_client.read_object(collection_addr)).resources[Collection] 31 | return { 32 | "creator": str(collection.creator), 33 | "name": str(collection.name), 34 | "description": str(collection.description), 35 | "uri": str(collection.uri), 36 | } # <:!:section_6 37 | 38 | 39 | # :!:>get_token_data 40 | async def get_token_data( 41 | token_client: AptosTokenClient, token_addr: AccountAddress 42 | ) -> dict[str, str]: 43 | token = (await token_client.read_object(token_addr)).resources[Token] 44 | return { 45 | "collection": str(token.collection), 46 | "description": str(token.description), 47 | "name": str(token.name), 48 | "uri": str(token.uri), 49 | "index": str(token.index), 50 | } # <:!:get_token_data 51 | 52 | 53 | async def main(): 54 | # Create API and faucet clients. 55 | # :!:>section_1a 56 | rest_client = RestClient(NODE_URL) 57 | faucet_client = FaucetClient( 58 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 59 | ) # <:!:section_1a 60 | 61 | # Create client for working with the token module. 62 | # :!:>section_1b 63 | token_client = AptosTokenClient(rest_client) # <:!:section_1b 64 | 65 | # :!:>section_2 66 | alice = Account.generate() 67 | bob = Account.generate() # <:!:section_2 68 | 69 | collection_name = "Alice's" 70 | token_name = "Alice's first token" 71 | 72 | # :!:>owners 73 | owners = {str(alice.address()): "Alice", str(bob.address()): "Bob"} # <:!:owners 74 | 75 | print("\n=== Addresses ===") 76 | print(f"Alice: {alice.address()}") 77 | print(f"Bob: {bob.address()}") 78 | 79 | # :!:>section_3 80 | bob_fund = faucet_client.fund_account(alice.address(), 100_000_000) 81 | alice_fund = faucet_client.fund_account(bob.address(), 100_000_000) # <:!:section_3 82 | await asyncio.gather(*[bob_fund, alice_fund]) 83 | 84 | print("\n=== Initial Coin Balances ===") 85 | alice_balance = rest_client.account_balance(alice.address()) 86 | bob_balance = rest_client.account_balance(bob.address()) 87 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 88 | print(f"Alice: {alice_balance}") 89 | print(f"Bob: {bob_balance}") 90 | 91 | print("\n=== Creating Collection and Token ===") 92 | 93 | # :!:>section_4 94 | txn_hash = await token_client.create_collection( 95 | alice, 96 | "Alice's simple collection", 97 | 1, 98 | collection_name, 99 | "https://aptos.dev", 100 | True, 101 | True, 102 | True, 103 | True, 104 | True, 105 | True, 106 | True, 107 | True, 108 | True, 109 | 0, 110 | 1, 111 | ) # <:!:section_4 112 | await rest_client.wait_for_transaction(txn_hash) 113 | 114 | collection_addr = AccountAddress.for_named_collection( 115 | alice.address(), collection_name 116 | ) 117 | 118 | collection_data = await get_collection_data(token_client, collection_addr) 119 | print( 120 | "\nCollection data: " 121 | + json.dumps({"address": str(collection_addr), **collection_data}, indent=4) 122 | ) 123 | 124 | # :!:>section_5 125 | txn_hash = await token_client.mint_token( 126 | alice, 127 | collection_name, 128 | "Alice's simple token", 129 | token_name, 130 | "https://aptos.dev/img/nyan.jpeg", 131 | PropertyMap([]), 132 | ) # <:!:section_5 133 | await rest_client.wait_for_transaction(txn_hash) 134 | 135 | minted_tokens = await token_client.tokens_minted_from_transaction(txn_hash) 136 | assert len(minted_tokens) == 1 137 | 138 | token_addr = minted_tokens[0] 139 | 140 | # Check the owner 141 | # :!:>section_7 142 | obj_resources = await token_client.read_object(token_addr) 143 | owner = str(get_owner(obj_resources)) 144 | print(f"\nToken owner: {owners[owner]}") # <:!:section_7 145 | token_data = await get_token_data(token_client, token_addr) 146 | print( 147 | "Token data: " 148 | + json.dumps( 149 | {"address": str(token_addr), "owner": owner, **token_data}, indent=4 150 | ) 151 | ) 152 | 153 | # Transfer the token to Bob 154 | # :!:>section_8 155 | print("\n=== Transferring the token to Bob ===") 156 | txn_hash = await token_client.transfer_token( 157 | alice, 158 | token_addr, 159 | bob.address(), 160 | ) 161 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_8 162 | 163 | # Read the object owner 164 | # :!:>section_9 165 | obj_resources = await token_client.read_object(token_addr) 166 | print(f"Token owner: {owners[str(get_owner(obj_resources))]}") # <:!:section_9 167 | 168 | # Transfer the token back to Alice 169 | # :!:>section_10 170 | print("\n=== Transferring the token back to Alice ===") 171 | txn_hash = await token_client.transfer_token( 172 | bob, 173 | token_addr, 174 | alice.address(), 175 | ) 176 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_10 177 | 178 | # Read the object owner one last time 179 | # :!:>section_11 180 | obj_resources = await token_client.read_object(token_addr) 181 | print(f"Token owner: {owners[str(get_owner(obj_resources))]}\n") # <:!:section_11 182 | 183 | await rest_client.close() 184 | 185 | 186 | if __name__ == "__main__": 187 | asyncio.run(main()) 188 | -------------------------------------------------------------------------------- /examples/simple_nft.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | import json 6 | 7 | from aptos_sdk.account import Account 8 | from aptos_sdk.aptos_tokenv1_client import AptosTokenV1Client 9 | from aptos_sdk.async_client import FaucetClient, RestClient 10 | 11 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 12 | 13 | 14 | async def main(): 15 | rest_client = RestClient(NODE_URL) 16 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 17 | token_client = AptosTokenV1Client(rest_client) 18 | 19 | # :!:>section_2 20 | alice = Account.generate() 21 | bob = Account.generate() # <:!:section_2 22 | 23 | collection_name = "Alice's" 24 | token_name = "Alice's first token" 25 | property_version = 0 26 | 27 | print("\n=== Addresses ===") 28 | print(f"Alice: {alice.address()}") 29 | print(f"Bob: {bob.address()}") 30 | 31 | # :!:>section_3 32 | bob_fund = faucet_client.fund_account(alice.address(), 100_000_000) 33 | alice_fund = faucet_client.fund_account(bob.address(), 100_000_000) # <:!:section_3 34 | await asyncio.gather(*[bob_fund, alice_fund]) 35 | 36 | print("\n=== Initial Coin Balances ===") 37 | alice_balance = rest_client.account_balance(alice.address()) 38 | bob_balance = rest_client.account_balance(bob.address()) 39 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 40 | print(f"Alice: {alice_balance}") 41 | print(f"Bob: {bob_balance}") 42 | 43 | print("\n=== Creating Collection and Token ===") 44 | 45 | # :!:>section_4 46 | txn_hash = await token_client.create_collection( 47 | alice, collection_name, "Alice's simple collection", "https://aptos.dev" 48 | ) # <:!:section_4 49 | await rest_client.wait_for_transaction(txn_hash) 50 | 51 | # :!:>section_5 52 | txn_hash = await token_client.create_token( 53 | alice, 54 | collection_name, 55 | token_name, 56 | "Alice's simple token", 57 | 1, 58 | "https://aptos.dev/img/nyan.jpeg", 59 | 0, 60 | ) # <:!:section_5 61 | await rest_client.wait_for_transaction(txn_hash) 62 | 63 | # :!:>section_6 64 | collection_data = await token_client.get_collection( 65 | alice.address(), collection_name 66 | ) 67 | print( 68 | f"Alice's collection: {json.dumps(collection_data, indent=4, sort_keys=True)}" 69 | ) # <:!:section_6 70 | # :!:>section_7 71 | balance = await token_client.get_token_balance( 72 | alice.address(), alice.address(), collection_name, token_name, property_version 73 | ) 74 | print(f"Alice's token balance: {balance}") # <:!:section_7 75 | # :!:>section_8 76 | token_data = await token_client.get_token_data( 77 | alice.address(), collection_name, token_name, property_version 78 | ) 79 | print( 80 | f"Alice's token data: {json.dumps(token_data, indent=4, sort_keys=True)}" 81 | ) # <:!:section_8 82 | 83 | print("\n=== Transferring the token to Bob ===") 84 | # :!:>section_9 85 | txn_hash = await token_client.offer_token( 86 | alice, 87 | bob.address(), 88 | alice.address(), 89 | collection_name, 90 | token_name, 91 | property_version, 92 | 1, 93 | ) # <:!:section_9 94 | await rest_client.wait_for_transaction(txn_hash) 95 | 96 | # :!:>section_10 97 | txn_hash = await token_client.claim_token( 98 | bob, 99 | alice.address(), 100 | alice.address(), 101 | collection_name, 102 | token_name, 103 | property_version, 104 | ) # <:!:section_10 105 | await rest_client.wait_for_transaction(txn_hash) 106 | 107 | alice_balance = token_client.get_token_balance( 108 | alice.address(), alice.address(), collection_name, token_name, property_version 109 | ) 110 | bob_balance = token_client.get_token_balance( 111 | bob.address(), alice.address(), collection_name, token_name, property_version 112 | ) 113 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 114 | print(f"Alice's token balance: {alice_balance}") 115 | print(f"Bob's token balance: {bob_balance}") 116 | 117 | print("\n=== Transferring the token back to Alice using MultiAgent ===") 118 | txn_hash = await token_client.direct_transfer_token( 119 | bob, alice, alice.address(), collection_name, token_name, 0, 1 120 | ) 121 | await rest_client.wait_for_transaction(txn_hash) 122 | 123 | alice_balance = token_client.get_token_balance( 124 | alice.address(), alice.address(), collection_name, token_name, property_version 125 | ) 126 | bob_balance = token_client.get_token_balance( 127 | bob.address(), alice.address(), collection_name, token_name, property_version 128 | ) 129 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 130 | print(f"Alice's token balance: {alice_balance}") 131 | print(f"Bob's token balance: {bob_balance}") 132 | 133 | await rest_client.close() 134 | 135 | 136 | if __name__ == "__main__": 137 | asyncio.run(main()) 138 | -------------------------------------------------------------------------------- /examples/simulate_transfer_coin.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | import json 6 | 7 | from aptos_sdk.account import Account 8 | from aptos_sdk.async_client import FaucetClient, RestClient 9 | from aptos_sdk.bcs import Serializer 10 | from aptos_sdk.transactions import ( 11 | EntryFunction, 12 | TransactionArgument, 13 | TransactionPayload, 14 | ) 15 | from aptos_sdk.type_tag import StructTag, TypeTag 16 | 17 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 18 | 19 | 20 | async def main(): 21 | rest_client = RestClient(NODE_URL) 22 | faucet_client = FaucetClient( 23 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 24 | ) # <:!:section_1 25 | 26 | alice = Account.generate() 27 | bob = Account.generate() 28 | 29 | print("\n=== Addresses ===") 30 | print(f"Alice: {alice.address()}") 31 | print(f"Bob: {bob.address()}") 32 | 33 | await faucet_client.fund_account(alice.address(), 100_000_000) 34 | 35 | payload = EntryFunction.natural( 36 | "0x1::coin", 37 | "transfer", 38 | [TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))], 39 | [ 40 | TransactionArgument(bob.address(), Serializer.struct), 41 | TransactionArgument(100_000, Serializer.u64), 42 | ], 43 | ) 44 | transaction = await rest_client.create_bcs_transaction( 45 | alice, TransactionPayload(payload) 46 | ) 47 | 48 | print("\n=== Simulate after creating Bob's Account ===") 49 | await faucet_client.fund_account(bob.address(), 1) 50 | output = await rest_client.simulate_transaction(transaction, alice) 51 | assert output[0]["vm_status"] == "Executed successfully", "This should succeed" 52 | print(json.dumps(output, indent=4, sort_keys=True)) 53 | 54 | await rest_client.close() 55 | 56 | 57 | if __name__ == "__main__": 58 | asyncio.run(main()) 59 | -------------------------------------------------------------------------------- /examples/transfer_coin.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | 6 | from aptos_sdk.account import Account 7 | from aptos_sdk.async_client import FaucetClient, IndexerClient, RestClient 8 | 9 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, INDEXER_URL, NODE_URL 10 | 11 | 12 | async def main(): 13 | # :!:>section_1 14 | rest_client = RestClient(NODE_URL) 15 | faucet_client = FaucetClient( 16 | FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN 17 | ) # <:!:section_1 18 | if INDEXER_URL and INDEXER_URL != "none": 19 | indexer_client = IndexerClient(INDEXER_URL) 20 | else: 21 | indexer_client = None 22 | 23 | # :!:>section_2 24 | alice = Account.generate() 25 | bob = Account.generate() # <:!:section_2 26 | 27 | print("\n=== Addresses ===") 28 | print(f"Alice: {alice.address()}") 29 | print(f"Bob: {bob.address()}") 30 | 31 | # :!:>section_3 32 | alice_fund = faucet_client.fund_account(alice.address(), 100_000_000) 33 | bob_fund = faucet_client.fund_account(bob.address(), 1) # <:!:section_3 34 | await asyncio.gather(*[alice_fund, bob_fund]) 35 | 36 | print("\n=== Initial Balances ===") 37 | # :!:>section_4 38 | alice_balance = rest_client.account_balance(alice.address()) 39 | bob_balance = rest_client.account_balance(bob.address()) 40 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 41 | print(f"Alice: {alice_balance}") 42 | print(f"Bob: {bob_balance}") # <:!:section_4 43 | 44 | # Have Alice give Bob 1_000 coins 45 | # :!:>section_5 46 | txn_hash = await rest_client.bcs_transfer( 47 | alice, bob.address(), 1_000 48 | ) # <:!:section_5 49 | # :!:>section_6 50 | await rest_client.wait_for_transaction(txn_hash) # <:!:section_6 51 | 52 | print("\n=== Intermediate Balances ===") 53 | alice_balance = rest_client.account_balance(alice.address()) 54 | bob_balance = rest_client.account_balance(bob.address()) 55 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 56 | print(f"Alice: {alice_balance}") 57 | print(f"Bob: {bob_balance}") # <:!:section_4 58 | 59 | # Have Alice give Bob another 1_000 coins using BCS 60 | txn_hash = await rest_client.bcs_transfer(alice, bob.address(), 1_000) 61 | await rest_client.wait_for_transaction(txn_hash) 62 | 63 | print("\n=== Final Balances ===") 64 | alice_balance = rest_client.account_balance(alice.address()) 65 | bob_balance = rest_client.account_balance(bob.address()) 66 | [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) 67 | print(f"Alice: {alice_balance}") 68 | print(f"Bob: {bob_balance}") 69 | 70 | if indexer_client: 71 | query = """ 72 | query TransactionsQuery($account: String) { 73 | account_transactions( 74 | limit: 20 75 | where: {account_address: {_eq: $account}} 76 | ) { 77 | transaction_version 78 | coin_activities { 79 | amount 80 | activity_type 81 | coin_type 82 | entry_function_id_str 83 | owner_address 84 | transaction_timestamp 85 | } 86 | } 87 | } 88 | """ 89 | 90 | variables = {"account": f"{bob.address()}"} 91 | data = await indexer_client.query(query, variables) 92 | assert len(data["data"]["account_transactions"]) > 0 93 | 94 | await rest_client.close() 95 | 96 | 97 | if __name__ == "__main__": 98 | asyncio.run(main()) 99 | -------------------------------------------------------------------------------- /examples/transfer_two_by_two.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import asyncio 5 | import os 6 | 7 | from aptos_sdk.account import Account 8 | from aptos_sdk.async_client import FaucetClient, RestClient 9 | from aptos_sdk.transactions import Script, ScriptArgument, TransactionPayload 10 | 11 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 12 | 13 | 14 | async def main(): 15 | rest_client = RestClient(NODE_URL) 16 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 17 | 18 | alice = Account.generate() 19 | bob = Account.generate() 20 | carol = Account.generate() 21 | david = Account.generate() 22 | 23 | print("\n=== Addresses ===") 24 | print(f"Alice: {alice.address()}") 25 | print(f"Bob: {bob.address()}") 26 | print(f"Carol: {carol.address()}") 27 | print(f"David: {david.address()}") 28 | 29 | alice_fund = faucet_client.fund_account(alice.address(), 100_000_000) 30 | bob_fund = faucet_client.fund_account(bob.address(), 100_000_000) 31 | carol_fund = faucet_client.fund_account(carol.address(), 1) 32 | david_fund = faucet_client.fund_account(david.address(), 1) 33 | await asyncio.gather(*[alice_fund, bob_fund, carol_fund, david_fund]) 34 | 35 | alice_balance = rest_client.account_balance(alice.address()) 36 | bob_balance = rest_client.account_balance(bob.address()) 37 | carol_balance = rest_client.account_balance(carol.address()) 38 | david_balance = rest_client.account_balance(david.address()) 39 | [alice_balance, bob_balance, carol_balance, david_balance] = await asyncio.gather( 40 | *[alice_balance, bob_balance, carol_balance, david_balance] 41 | ) 42 | 43 | print("\n=== Initial Balances ===") 44 | print(f"Alice: {alice_balance}") 45 | print(f"Bob: {bob_balance}") 46 | print(f"Carol: {carol_balance}") 47 | print(f"David: {david_balance}") 48 | 49 | path = os.path.dirname(__file__) 50 | filepath = os.path.join(path, "two_by_two_transfer.mv") 51 | with open(filepath, mode="rb") as file: 52 | code = file.read() 53 | 54 | script_arguments = [ 55 | ScriptArgument(ScriptArgument.U64, 100), 56 | ScriptArgument(ScriptArgument.U64, 200), 57 | ScriptArgument(ScriptArgument.ADDRESS, carol.address()), 58 | ScriptArgument(ScriptArgument.ADDRESS, david.address()), 59 | ScriptArgument(ScriptArgument.U64, 50), 60 | ] 61 | 62 | payload = TransactionPayload(Script(code, [], script_arguments)) 63 | txn = await rest_client.create_multi_agent_bcs_transaction(alice, [bob], payload) 64 | txn_hash = await rest_client.submit_bcs_transaction(txn) 65 | await rest_client.wait_for_transaction(txn_hash) 66 | 67 | alice_balance = rest_client.account_balance(alice.address()) 68 | bob_balance = rest_client.account_balance(bob.address()) 69 | carol_balance = rest_client.account_balance(carol.address()) 70 | david_balance = rest_client.account_balance(david.address()) 71 | [alice_balance, bob_balance, carol_balance, david_balance] = await asyncio.gather( 72 | *[alice_balance, bob_balance, carol_balance, david_balance] 73 | ) 74 | 75 | print("\n=== Final Balances ===") 76 | print(f"Alice: {alice_balance}") 77 | print(f"Bob: {bob_balance}") 78 | print(f"Carol: {carol_balance}") 79 | print(f"David: {david_balance}") 80 | 81 | await rest_client.close() 82 | 83 | 84 | if __name__ == "__main__": 85 | asyncio.run(main()) 86 | -------------------------------------------------------------------------------- /examples/two_by_two_transfer.mv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aptos-labs/aptos-python-sdk/f0da7066f1d48f81f695b62a49a707a8a4897323/examples/two_by_two_transfer.mv -------------------------------------------------------------------------------- /examples/your_coin.py: -------------------------------------------------------------------------------- 1 | # Copyright © Aptos Foundation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This example depends on the MoonCoin.move module having already been published to the destination blockchain. 6 | 7 | One method to do so is to use the CLI: 8 | * Acquire the Aptos CLI, see https://aptos.dev/cli-tools/aptos-cli/use-cli/install-aptos-cli 9 | * `python -m examples.your-coin ~/aptos-core/aptos-move/move-examples/moon_coin`. 10 | * Open another terminal and `aptos move compile --package-dir ~/aptos-core/aptos-move/move-examples/moon_coin --save-metadata --named-addresses MoonCoin=`. 11 | * Return to the first terminal and press enter. 12 | """ 13 | 14 | import asyncio 15 | import os 16 | import sys 17 | 18 | from aptos_sdk.account import Account 19 | from aptos_sdk.account_address import AccountAddress 20 | from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper 21 | from aptos_sdk.async_client import FaucetClient, RestClient 22 | from aptos_sdk.bcs import Serializer 23 | from aptos_sdk.package_publisher import PackagePublisher 24 | from aptos_sdk.transactions import ( 25 | EntryFunction, 26 | TransactionArgument, 27 | TransactionPayload, 28 | ) 29 | from aptos_sdk.type_tag import StructTag, TypeTag 30 | 31 | from .common import FAUCET_AUTH_TOKEN, FAUCET_URL, NODE_URL 32 | 33 | 34 | class CoinClient(RestClient): 35 | async def register_coin(self, coin_address: AccountAddress, sender: Account) -> str: 36 | """Register the receiver account to receive transfers for the new coin.""" 37 | 38 | payload = EntryFunction.natural( 39 | "0x1::managed_coin", 40 | "register", 41 | [TypeTag(StructTag.from_str(f"{coin_address}::moon_coin::MoonCoin"))], 42 | [], 43 | ) 44 | signed_transaction = await self.create_bcs_signed_transaction( 45 | sender, TransactionPayload(payload) 46 | ) 47 | return await self.submit_bcs_transaction(signed_transaction) 48 | 49 | async def mint_coin( 50 | self, minter: Account, receiver_address: AccountAddress, amount: int 51 | ) -> str: 52 | """Mints the newly created coin to a specified receiver address.""" 53 | 54 | payload = EntryFunction.natural( 55 | "0x1::managed_coin", 56 | "mint", 57 | [TypeTag(StructTag.from_str(f"{minter.address()}::moon_coin::MoonCoin"))], 58 | [ 59 | TransactionArgument(receiver_address, Serializer.struct), 60 | TransactionArgument(amount, Serializer.u64), 61 | ], 62 | ) 63 | signed_transaction = await self.create_bcs_signed_transaction( 64 | minter, TransactionPayload(payload) 65 | ) 66 | return await self.submit_bcs_transaction(signed_transaction) 67 | 68 | async def get_balance( 69 | self, 70 | coin_address: AccountAddress, 71 | account_address: AccountAddress, 72 | ) -> int: 73 | """Returns the coin balance of the given account""" 74 | 75 | return await self.account_balance( 76 | account_address, 77 | None, 78 | f"{coin_address}::moon_coin::MoonCoin", 79 | ) 80 | 81 | 82 | async def main(moon_coin_path: str): 83 | alice = Account.generate() 84 | bob = Account.generate() 85 | 86 | print("\n=== Addresses ===") 87 | print(f"Alice: {alice.address()}") 88 | print(f"Bob: {bob.address()}") 89 | 90 | rest_client = CoinClient(NODE_URL) 91 | faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) 92 | 93 | alice_fund = faucet_client.fund_account(alice.address(), 20_000_000) 94 | bob_fund = faucet_client.fund_account(bob.address(), 20_000_000) 95 | await asyncio.gather(*[alice_fund, bob_fund]) 96 | 97 | if AptosCLIWrapper.does_cli_exist(): 98 | AptosCLIWrapper.compile_package(moon_coin_path, {"MoonCoin": alice.address()}) 99 | else: 100 | input("\nUpdate the module with Alice's address, compile, and press enter.") 101 | 102 | # :!:>publish 103 | module_path = os.path.join( 104 | moon_coin_path, "build", "Examples", "bytecode_modules", "moon_coin.mv" 105 | ) 106 | with open(module_path, "rb") as f: 107 | module = f.read() 108 | 109 | metadata_path = os.path.join( 110 | moon_coin_path, "build", "Examples", "package-metadata.bcs" 111 | ) 112 | with open(metadata_path, "rb") as f: 113 | metadata = f.read() 114 | 115 | print("\nPublishing MoonCoin package.") 116 | package_publisher = PackagePublisher(rest_client) 117 | txn_hash = await package_publisher.publish_package(alice, metadata, [module]) 118 | await rest_client.wait_for_transaction(txn_hash) 119 | # <:!:publish 120 | 121 | print("\nBob registers the newly created coin so he can receive it from Alice.") 122 | txn_hash = await rest_client.register_coin(alice.address(), bob) 123 | await rest_client.wait_for_transaction(txn_hash) 124 | balance = await rest_client.get_balance(alice.address(), bob.address()) 125 | print(f"Bob's initial MoonCoin balance: {balance}") 126 | 127 | print("Alice mints Bob some of the new coin.") 128 | txn_hash = await rest_client.mint_coin(alice, bob.address(), 100) 129 | await rest_client.wait_for_transaction(txn_hash) 130 | balance = await rest_client.get_balance(alice.address(), bob.address()) 131 | print(f"Bob's updated MoonCoin balance: {balance}") 132 | 133 | try: 134 | maybe_balance = await rest_client.get_balance(alice.address(), alice.address()) 135 | except Exception: 136 | maybe_balance = None 137 | print(f"Bob will transfer to Alice, her balance: {maybe_balance}") 138 | txn_hash = await rest_client.transfer_coins( 139 | bob, alice.address(), f"{alice.address()}::moon_coin::MoonCoin", 5 140 | ) 141 | await rest_client.wait_for_transaction(txn_hash) 142 | balance = await rest_client.get_balance(alice.address(), alice.address()) 143 | print(f"Alice's updated MoonCoin balance: {balance}") 144 | balance = await rest_client.get_balance(alice.address(), bob.address()) 145 | print(f"Bob's updated MoonCoin balance: {balance}") 146 | 147 | await rest_client.close() 148 | 149 | 150 | if __name__ == "__main__": 151 | assert ( 152 | len(sys.argv) == 2 153 | ), "Expecting an argument that points to the moon_coin directory." 154 | 155 | asyncio.run(main(sys.argv[1])) 156 | -------------------------------------------------------------------------------- /features/account_address.feature: -------------------------------------------------------------------------------- 1 | Feature: Account Address 2 | """ 3 | AccountAddress is a 32-byte value that represents an address on chain. 4 | """ 5 | 6 | Scenario Outline: Parse account address