diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml new file mode 100644 index 0000000..79ca979 --- /dev/null +++ b/.github/workflows/examples.yaml @@ -0,0 +1,76 @@ +name: Examples on Devnet + +# Runs every example in `examples/` against Devnet with `make examples`. The examples fund the accounts they generate +# from the account of the repository secret SUPRA_EXAMPLES_PRIVATE_KEY (an Ed25519 private key, 32 bytes as hex, of an +# account funded on Devnet). Without the secret, or with a secret that is only whitespace, the job skips the examples +# and reports a notice. + +on: + schedule: + - cron: "41 4 * * *" + workflow_dispatch: + +permissions: + contents: read + +# Two runs sending transactions from the same account at once would race for its sequence numbers. +concurrency: + group: examples-devnet + cancel-in-progress: false + +jobs: + examples: + name: Examples on Devnet + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + SUPRA_RPC_NODE_URL: https://rpc-devnet.supra.com + UV_PYTHON: "3.12" + + steps: + # The examples treat a key that is empty or only whitespace as unset, so this step decides the same way. + # The secret reaches the step through `env`; the step writes only whether a key is present. + - name: Check for a funded key + id: key + env: + KEY: ${{ secrets.SUPRA_EXAMPLES_PRIVATE_KEY }} + run: | + stripped="${KEY//[[:space:]]/}" + if [ -n "$stripped" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + fi + + - name: Skip without a funded key + if: steps.key.outputs.present != 'true' + run: echo "::notice title=Examples skipped::The repository secret SUPRA_EXAMPLES_PRIVATE_KEY is not set, so no example ran." + + - name: Checkout code + if: steps.key.outputs.present == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install uv + if: steps.key.outputs.present == 'true' + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "0.13.7" + python-version: "3.12" + + - name: Install dependencies + if: steps.key.outputs.present == 'true' + run: uv sync --frozen + + # The uv release and the packages are fixed before the key is present: setup-uv installs the pinned uv, and + # the step above installs the packages from uv.lock. The key is visible to this step only. UV_FROZEN makes + # `uv run` use uv.lock without resolving, and UV_NO_SYNC makes it skip syncing the environment, so nothing is + # resolved, downloaded or installed while the key is present. + - name: Run the examples + if: steps.key.outputs.present == 'true' + env: + SUPRA_EXAMPLES_PRIVATE_KEY: ${{ secrets.SUPRA_EXAMPLES_PRIVATE_KEY }} + UV_FROZEN: "1" + UV_NO_SYNC: "1" + run: make examples diff --git a/Makefile b/Makefile index 5e8718f..99a5ab7 100644 --- a/Makefile +++ b/Makefile @@ -39,18 +39,31 @@ audit: ci: lint fmt-check typecheck test-unit test-spec test-coverage build audit +# `make examples` runs every example, in this order, against the node at +# SUPRA_RPC_NODE_URL. It runs all of them even when one fails, then fails if any +# failed and lists those. The examples send transactions, so the target needs +# SUPRA_RPC_NODE_URL set; see examples/README.md for the other variables. For example: +# SUPRA_RPC_NODE_URL=http://localhost:27001 make examples +# SUPRA_RPC_NODE_URL=https://rpc-devnet.supra.com SUPRA_EXAMPLES_PRIVATE_KEY= make examples +EXAMPLES = automation faucet fee_payer_transfer_coin fungible_asset hello_blockchain multisig \ + rotate_key simple_nft simple_supra_token simulate_transfer_coin supra_token \ + transfer_coin transfer_two_by_two your_coin + examples: - uv run python -m examples.automation - uv run python -m examples.fee_payer_transfer_coin - uv run python -m examples.multisig - uv run python -m examples.rotate_key - uv run python -m examples.simple_nft - uv run python -m examples.simple_supra_token - uv run python -m examples.simulate_transfer_coin - uv run python -m examples.supra_token - uv run python -m examples.transfer_coin - uv run python -m examples.transfer_two_by_two - uv run python -m examples.your_coin + @if [ -z "$(strip $(SUPRA_RPC_NODE_URL))" ]; then \ + echo "Set SUPRA_RPC_NODE_URL to the node to run the examples against (see examples/README.md)"; \ + exit 1; \ + fi + @failed=""; \ + for example in $(EXAMPLES); do \ + echo "=== examples.$$example ==="; \ + uv run python -m examples.$$example || failed="$$failed $$example"; \ + done; \ + if [ -n "$$failed" ]; then \ + echo "Failed examples:$$failed"; \ + exit 1; \ + fi; \ + echo "All examples passed" generate-api-docs: uv run sphinx-apidoc -o docs supra_sdk diff --git a/examples/README.md b/examples/README.md index 6c2413e..bf2cdc7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,30 +1,133 @@ # Examples -This section provides **practical, real-world examples** demonstrating how to use the **`supra-python-sdk`** for most of the significant operations. - -Each example is **self-contained**, focusing on a single use case, making it easy to understand and adapt for your own projects. +Runnable examples of the **`supra-python-sdk`**. Each example covers one use case: coin transfers, sponsored and +multi-agent transactions, simulation, key rotation, multisig accounts, tokens, publishing Move packages, automation +tasks and the faucet. ## Requirements -Before running the examples, ensure you have the following: +- Python 3.12 or later. +- [`uv`](https://docs.astral.sh/uv/) ([installation guide](https://docs.astral.sh/uv/getting-started/installation/)). + +The examples need only the SDK's own dependencies. They do not need a Move toolchain: the Move packages they publish +are committed in compiled form (see [Move packages](#move-packages)). + +## Networks + +The examples run against Devnet by default (`https://rpc-devnet.supra.com`). Set `SUPRA_RPC_NODE_URL` to run them +against another network, for example Testnet (`https://rpc-testnet.supra.com`) or a localnet +(`http://localhost:27001`). The examples refuse to send transactions to Mainnet. + +## Environment + +| Variable | Meaning | +|---|---| +| `SUPRA_RPC_NODE_URL` | Base URL of the RPC node, without `/rpc/v3`. Default `https://rpc-devnet.supra.com` (Devnet). | +| `SUPRA_FAUCET_URL` | Base URL of the node that serves the faucet. Default `SUPRA_RPC_NODE_URL`. | +| `SUPRA_EXAMPLES_PRIVATE_KEY` | Ed25519 private key (32 bytes as hex, with or without `0x`) of a funded account. Optional. | + +An empty or whitespace-only variable counts as unset. -- **Python 3.12+** installed on your system. -- **[`uv`](https://docs.astral.sh/uv/)** – used for packaging and dependency management. +Every example generates fresh accounts. It funds them in one of two ways: -### Installing `uv` +- With `SUPRA_EXAMPLES_PRIVATE_KEY` set, the account of that key sends SUPRA to each generated account. No faucet is + called (except by `faucet.py`, which demonstrates the faucet). The key is never printed; the examples print the + account's address. +- Without it, the example funds one generated account from the faucet and that account sends SUPRA to the other + generated accounts. The faucet can answer `TryLater` when it cannot serve the request yet. Every example except + `faucet.py` then sends the request again after 5, 10, 20 and 40 seconds, so it sends at most five requests, and fails + after the fifth `TryLater`. It waits for the faucet transaction once, after the faucet accepts a request. + `faucet.py` sends one request and does not send it again. -Follow the official [installation guide](https://docs.astral.sh/uv/getting-started/installation/) to install `uv` on your system. +Before funding, an example checks that the funding account holds the SUPRA it is about to send plus 2,000 units of gas +per transfer at the node's minimum gas price, and otherwise stops with the shortfall in SUPRA. -## Running an Example +The Devnet faucet limits the requests from one IP address, so running more than one example against Devnet needs +`SUPRA_EXAMPLES_PRIVATE_KEY`. A localnet faucet has no such limit. Each generated account receives 10 SUPRA +(`multisig.py`: 30 SUPRA for each account that sends transactions, 1 SUPRA for each signer); a full run of +`make examples` takes about 325 SUPRA from the funding account, most of it left in the generated accounts. -You can run any example using the following syntax: +Two runs that use the same `SUPRA_EXAMPLES_PRIVATE_KEY` at the same time send transactions from the same account and +race for its sequence numbers, so their funding transfers can fail. Run one at a time per key. + +Keep private keys in environment variables or an untracked file. Do not commit them. + +## Running + +One example, from the repository root: ```bash uv run python -m examples. ``` -Running `transfer_coin.py` example: +For example: + +```bash +# Devnet, accounts funded from a funded account +SUPRA_EXAMPLES_PRIVATE_KEY= uv run python -m examples.transfer_coin + +# A localnet, accounts funded from the localnet faucet +SUPRA_RPC_NODE_URL=http://localhost:27001 uv run python -m examples.transfer_coin +``` + +Every example, in sequence. `make examples` runs all of them even when one fails, then exits with a non-zero status +and lists the failed examples if any failed. It needs `SUPRA_RPC_NODE_URL` set, so that the network the examples send +transactions to is chosen explicitly: + +```bash +SUPRA_RPC_NODE_URL=https://rpc-devnet.supra.com SUPRA_EXAMPLES_PRIVATE_KEY= make examples +SUPRA_RPC_NODE_URL=http://localhost:27001 make examples +``` + +An example exits with a non-zero status when a step raises, which includes every transaction that does not end with +status `Success`. + +## The examples + +| Example | Shows | +|---|---| +| [`automation.py`](./automation.py) | Registering an automation task and stopping it | +| [`faucet.py`](./faucet.py) | One faucet request, sent without waiting, then waiting for the faucet transaction | +| [`fee_payer_transfer_coin.py`](./fee_payer_transfer_coin.py) | A coin transfer whose gas a sponsor pays, simulated and then submitted | +| [`fungible_asset.py`](./fungible_asset.py) | Reading a SUPRA balance as a fungible asset, migrating a `CoinStore` balance when the fungible balance is 0, a fungible asset transfer, and walking the receiver's fungible store transactions | +| [`hello_blockchain.py`](./hello_blockchain.py) | Publishing the `hello_blockchain` package and calling its entry function; `python -m examples.hello_blockchain
` uses a package already published at `
` | +| [`multisig.py`](./multisig.py) | A 2-of-3 MultiEd25519 account: a transfer, rotating an account's key to the multisig key, publishing and upgrading the `upgrade_and_govern` package, and a Move script; `python -m examples.multisig true` pauses between the steps | +| [`rotate_key.py`](./rotate_key.py) | Rotating an account's authentication key to another Ed25519 key, then to a MultiEd25519 key | +| [`simple_nft.py`](./simple_nft.py) | A token collection and token of `0x3::token`: create, offer, claim, and a multi-agent transfer | +| [`simple_supra_token.py`](./simple_supra_token.py) | A collection and token of the object-based token standard, and token transfers | +| [`simulate_transfer_coin.py`](./simulate_transfer_coin.py) | Simulating a transfer before and after the receiver has a coin store | +| [`supra_token.py`](./supra_token.py) | Adding, removing and updating the properties of an object-based token, and transferring it | +| [`transfer_coin.py`](./transfer_coin.py) | SUPRA transfers and balances | +| [`transfer_two_by_two.py`](./transfer_two_by_two.py) | A multi-agent Move script (`two_by_two_transfer.mv`) with two senders and two receivers | +| [`your_coin.py`](./your_coin.py) | Publishing the `moon_coin` package, a managed coin, and registering, minting and transferring the coin | + +[`common.py`](./common.py) reads the environment and funds the generated accounts. + +## Move packages + +[`move/packages.json`](./move/packages.json) holds the compiled packages that `hello_blockchain.py`, `multisig.py` and +`your_coin.py` publish: for each package, the package metadata, the module bytecode and the script bytecode, as hex. +They were compiled for the address named in the file, and `compiled_package` of `common.py` replaces that address +with the publisher's in the bytecode before publishing. The metadata contains no address. + +The sources are in [`Entropy-Foundation/aptos-core`](https://github.com/Entropy-Foundation/aptos-core) at the tag +`aptosvm-v1.16_supra-v1.8.9`, and are licensed under Apache-2.0; the compiled packages derive from them: + +| Package in `packages.json` | Source directory | Named address | +|---|---|---| +| `hello_blockchain` | `aptos-move/move-examples/hello_blockchain` | `hello_blockchain` | +| `moon_coin` | `aptos-move/move-examples/moon_coin` | `MoonCoin` | +| `upgrade_and_govern_genesis` | `aptos-move/move-examples/upgrade_and_govern/genesis` | `upgrade_and_govern` | +| `upgrade_and_govern_upgrade` | `aptos-move/move-examples/upgrade_and_govern/upgrade` | `upgrade_and_govern` | + +Each was compiled with the `supra` CLI of node release v11.7.2, against the framework packages +(`aptos-move/framework/*`) of the same tag, with: ```bash -uv run python -m examples.transfer_coin +supra move tool compile --save-metadata --package-dir \ + --named-addresses =0x9c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e ``` + +The outputs are `build//package-metadata.bcs`, `build//bytecode_modules/*.mv` and +`build//bytecode_scripts/*.mv`; `packages.json` lists the modules in the order of publication +(`parameters` before `transfer`) and, for `upgrade_and_govern_upgrade`, the script `set_and_transfer`. diff --git a/examples/automation.py b/examples/automation.py index ac4518b..82fabaa 100644 --- a/examples/automation.py +++ b/examples/automation.py @@ -4,29 +4,47 @@ import asyncio import json import time +from collections.abc import Awaitable, Callable -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.bcs import Serializer -from supra_sdk.clients.rest import SupraClient, SupraClientConfig +from supra_sdk.clients.rest import SupraClient +from supra_sdk.clients.rest.supra_client import TransactionFailedError +from supra_sdk.clients.rest.transaction_defaults import MaxGasEstimationError from supra_sdk.transactions import EntryFunction, TransactionArgument +TASK_DURATION_SECONDS = 3600 -async def main(): - supra_client_config = SupraClientConfig( - expiration_ttl=300, - gas_unit_price=100_000, - max_gas_amount=100_000, - ) - supra_client = SupraClient(RPC_NODE_URL, supra_client_config) +# The registry refuses registrations and stops while it moves from one automation cycle +# to the next. The example sends the transaction again after a pause, this many times. +CYCLE_TRANSITION_ATTEMPTS = 10 +CYCLE_TRANSITION_PAUSE_SECONDS = 3.0 + + +async def outside_cycle_transition(action: Callable[[], Awaitable[str]]) -> str: + """Runs `action` again while the registry reports a cycle transition in progress.""" + for attempt in range(1, CYCLE_TRANSITION_ATTEMPTS + 1): + try: + return await action() + except (TransactionFailedError, MaxGasEstimationError) as error: + in_transition = "ECYCLE_TRANSITION_IN_PROGRESS" in str(error.vm_status) + if not in_transition or attempt == CYCLE_TRANSITION_ATTEMPTS: + raise + print("Automation cycle transition in progress, trying again") + await asyncio.sleep(CYCLE_TRANSITION_PAUSE_SECONDS) + raise AssertionError("unreachable") + + +async def main() -> None: + supra_client = SupraClient(RPC_NODE_URL) sender = Account.generate() print(f"Automation task creator account address: {sender.address()}") - await supra_client.faucet(sender.address()) - print(f"Faucet on {sender.address()} done") + await fund_accounts(supra_client, [sender.address()]) - print("Registering automation tasK") + print("Registering automation task") automated_function = EntryFunction.natural( "0x1::supra_account", "transfer", @@ -49,14 +67,21 @@ async def main(): max(gas_prices["min_configured_gas_price"], gas_prices["median_gas_price"]), gas_prices["max_gas_price"], ) - task_registration_tx_hash = await supra_client.register_automation_task( - sender, - automated_function, - 9, - gas_price_cap, - 1 * (10**8), - int(time.time()) + 7200, - [], + # The registry refuses a task whose duration from the block time of its + # registration exceeds `task_duration_cap_in_secs` of + # `0x1::automation_registry::ActiveAutomationRegistryConfigV2` (7200 seconds on a + # localnet), and a task that expires before the end of the next automation cycle. + expiry_time = int(time.time()) + TASK_DURATION_SECONDS + task_registration_tx_hash = await outside_cycle_transition( + lambda: supra_client.register_automation_task( + sender, + automated_function, + 9, + gas_price_cap, + 1 * (10**8), + expiry_time, + [], + ) ) # The registration already succeeded (the helper raised otherwise); fetch its events to read the task index. @@ -74,7 +99,9 @@ async def main(): print(f"Task registered successfully, task_id: {task_index}") print(f"Stopping task, task_index: {task_index}") - await supra_client.stop_automation_tasks(sender, [int(task_index)]) + await outside_cycle_transition( + lambda: supra_client.stop_automation_tasks(sender, [int(task_index)]) + ) print(f"Task stopped successfully, task_index: {task_index}") await supra_client.close() diff --git a/examples/common.py b/examples/common.py index d60e514..7dc198b 100644 --- a/examples/common.py +++ b/examples/common.py @@ -2,11 +2,217 @@ # Parts of the project are originally copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +"""Settings and helpers shared by the examples. + +The examples read the network and the funding account from the environment: + +- `SUPRA_RPC_NODE_URL`: the base URL of the RPC node (without `/rpc/v3`). Default + `https://rpc-devnet.supra.com` (Devnet). +- `SUPRA_FAUCET_URL`: the base URL of the node that serves the faucet. Default + `SUPRA_RPC_NODE_URL`. +- `SUPRA_EXAMPLES_PRIVATE_KEY`: the Ed25519 private key (32 bytes as hex, with or + without `0x`) of a funded account. When it is set, the examples fund the accounts + they generate by transfers from this account and do not call the faucet. When it is + not set, an example funds one generated account from the faucet and funds the other + accounts it generates by transfers from that account. It sends one faucet request, + and sends it again, at most four times, while the faucet answers "TryLater". + +An empty or whitespace-only variable counts as unset. The examples do not send +transactions to Mainnet. +""" + +import asyncio +import json import os +import time +from dataclasses import dataclass +from pathlib import Path + +from supra_sdk.account import Account +from supra_sdk.account_address import AccountAddress +from supra_sdk.clients.rest import SupraClient +from supra_sdk.clients.rest.supra_client import FaucetRequestNotAcceptedError + +DEVNET_RPC_URL = "https://rpc-devnet.supra.com" + +RPC_NODE_URL = (os.getenv("SUPRA_RPC_NODE_URL") or "").strip() or DEVNET_RPC_URL +FAUCET_URL = (os.getenv("SUPRA_FAUCET_URL") or "").strip() or RPC_NODE_URL + +PRIVATE_KEY_VARIABLE = "SUPRA_EXAMPLES_PRIVATE_KEY" + +MAINNET_CHAIN_ID = 8 + +QUANTS_PER_SUPRA = 10**8 + +# SUPRA given to each account that `fund_accounts` funds, unless the example asks for +# another amount. It pays for the gas of a few transactions at the minimum gas price. +DEFAULT_FUNDING_QUANTS = 10 * QUANTS_PER_SUPRA + +# Gas units reserved for each funding transfer when checking the funding account's +# balance. The node accepts a transfer only if the balance covers the amount plus the +# max gas at the gas price, and the SDK sets the max gas to 1.5 times the gas the +# simulated transfer uses, with no upper bound. A transfer to a new account uses about +# 1,000 units on Devnet today, so the estimate is about 1,500 to 1,600 units; the +# reserve covers an estimate of up to 2,000. The check is made before sending, and a +# transfer can still fail if the gas it uses rises. +FUNDING_TRANSFER_GAS_UNITS = 2_000 + +# Pauses before the second to fifth faucet request, sent while the faucet answers +# "TryLater". After the fifth request the example gives up. +FAUCET_RETRY_PAUSES_SECONDS = (5.0, 10.0, 20.0, 40.0) + +# How long to wait for the faucet's funds to show in the balance, and how often to look. +FAUCET_BALANCE_TIMEOUT_SECONDS = 180.0 +FAUCET_BALANCE_POLL_SECONDS = 2.0 + +PACKAGES_PATH = Path(__file__).parent / "move" / "packages.json" + +# The account that funds the accounts of this example, once it is known. +_funding_account: Account | None = None + + +async def fund_accounts( + supra_client: SupraClient, + addresses: list[AccountAddress], + quants: int = DEFAULT_FUNDING_QUANTS, +) -> None: + """Sends `quants` of SUPRA to each of `addresses` from the funding account. + + The funding account is the account of `SUPRA_EXAMPLES_PRIVATE_KEY` when it is set. + Otherwise it is an account generated on the first call and funded from the faucet + at `SUPRA_FAUCET_URL`; later calls in the same process reuse it. + + Before sending, it checks that the funding account's balance covers the transfers: + `quants` for each address plus `FUNDING_TRANSFER_GAS_UNITS` of gas for each at the + node's minimum gas price. + + Raises: + RuntimeError: If the node is a Mainnet node, or the funding account's balance + does not cover the transfers. + + """ + if await supra_client.chain_id() == MAINNET_CHAIN_ID: + raise RuntimeError("The examples do not send transactions to Mainnet") + funder = await _funder(supra_client) + + gas_unit_price = (await supra_client.estimate_gas_price())[ + "min_configured_gas_price" + ] + needed = len(addresses) * (quants + FUNDING_TRANSFER_GAS_UNITS * gas_unit_price) + balance = await supra_client.account_supra_balance(funder.address()) + if balance < needed: + source = f" (the account of {PRIVATE_KEY_VARIABLE})" if _private_key() else "" + raise RuntimeError( + f"The funding account {funder.address()}{source} holds " + f"{_supra(balance)} SUPRA; funding {len(addresses)} account(s) needs " + f"{_supra(needed)} SUPRA, {_supra(needed - balance)} SUPRA more" + ) + + for address in addresses: + await supra_client.transfer_supra_coin(funder, address, quants) + print(f"Funded {address} with {quants} quants from {funder.address()}") + + +def _supra(quants: int) -> str: + """Formats an amount of quants as SUPRA, with all eight decimals.""" + return f"{quants // QUANTS_PER_SUPRA}.{quants % QUANTS_PER_SUPRA:08d}" + + +def _private_key() -> str: + """The value of SUPRA_EXAMPLES_PRIVATE_KEY; empty or whitespace counts as unset.""" + return (os.getenv(PRIVATE_KEY_VARIABLE) or "").strip() + + +async def _funder(supra_client: SupraClient) -> Account: + global _funding_account + if _funding_account is not None: + return _funding_account + + private_key = _private_key() + if private_key: + funder = Account.load_key(private_key) + else: + funder = Account.generate() + await _fund_from_faucet(supra_client, funder.address()) + print(f"Funding account: {funder.address()}") + _funding_account = funder + return funder + + +async def _fund_from_faucet(supra_client: SupraClient, address: AccountAddress) -> None: + """Funds `address` from the faucet and waits until its balance shows the funds.""" + faucet_client = ( + supra_client if FAUCET_URL == RPC_NODE_URL else SupraClient(FAUCET_URL) + ) + try: + faucet_tx_hash = None + for pause in (*FAUCET_RETRY_PAUSES_SECONDS, None): + try: + faucet_tx_hash = await faucet_client.faucet( + address, wait_for_faucet=False + ) + # SDK versions that do not handle a "TryLater" body raise AttributeError + # from the request for it. Nothing else in this call is caught. + except (FaucetRequestNotAcceptedError, AttributeError): + faucet_tx_hash = None + if faucet_tx_hash: + break + # The faucet answered "TryLater" (the request returned no hash). + if pause is None: + break + print(f"The faucet answered TryLater; requesting again in {pause:g} s") + await asyncio.sleep(pause) + + if not faucet_tx_hash: + raise FaucetRequestNotAcceptedError + + # The request was accepted; wait for its transaction once, outside the retry. + faucet_data = await faucet_client.wait_for_faucet(faucet_tx_hash) + if faucet_data.get("status") != "Success": + raise RuntimeError( + f"The faucet transaction {faucet_tx_hash} ended with status " + f"{faucet_data.get('status')}" + ) + finally: + if faucet_client is not supra_client: + await faucet_client.close() + + # The faucet may be served by another node than `supra_client`'s. + deadline = time.monotonic() + FAUCET_BALANCE_TIMEOUT_SECONDS + while await supra_client.account_supra_balance(address) == 0: + if time.monotonic() > deadline: + raise RuntimeError(f"{address} holds no SUPRA after the faucet request") + await asyncio.sleep(FAUCET_BALANCE_POLL_SECONDS) + + +@dataclass(frozen=True) +class CompiledPackage: + """A Move package of `move/packages.json`, compiled for one address.""" + + metadata: bytes + modules: list[bytes] + scripts: list[bytes] + + +def compiled_package(name: str, address: AccountAddress) -> CompiledPackage: + """Returns the package `name` of `move/packages.json` compiled for `address`. + + The packages in the file were compiled for the address it names. Every occurrence of + that address in the bytecode (the address of the package's modules, and constants + equal to it) is replaced by `address`. The metadata contains no address. + """ + data = json.loads(PACKAGES_PATH.read_text()) + placeholder = AccountAddress.from_str(data["address"]).address + package = data["packages"][name] + + def relocate(code_hex: str) -> bytes: + code = bytes.fromhex(code_hex) + if placeholder not in code: + raise ValueError(f"The bytecode of {name} does not contain its address") + return code.replace(placeholder, address.address) -SUPRA_CORE_PATH = os.getenv("SUPRA_CORE_PATH", os.path.abspath("../aptos-core")) -FAUCET_URL = os.getenv( - "SUPRA_FAUCET_URL", - "http://localhost:27001/", -) -RPC_NODE_URL = os.getenv("SUPRA_RPC_NODE_URL", "http://localhost:27001/") + return CompiledPackage( + metadata=bytes.fromhex(package["metadata"]), + modules=[relocate(module) for module in package["modules"]], + scripts=[relocate(script) for script in package["scripts"]], + ) diff --git a/examples/faucet.py b/examples/faucet.py index 9123247..9b44075 100644 --- a/examples/faucet.py +++ b/examples/faucet.py @@ -1,6 +1,13 @@ # Copyright © Supra # SPDX-License-Identifier: Apache-2.0 +"""Funds a new account from the faucet at `SUPRA_FAUCET_URL` with one faucet request. + +`SupraClient.faucet` waits for the faucet transaction by default. This example sends +the request without waiting and then waits for the transaction with +`SupraClient.wait_for_faucet`. +""" + import asyncio from examples.common import FAUCET_URL @@ -8,27 +15,21 @@ from supra_sdk.clients.rest import SupraClient -async def main(): +async def main() -> None: supra_client = SupraClient(FAUCET_URL) alice = Account.generate() - bob = Account.generate() + print(f"Alice account address: {alice.address()}") + + tx_hash = await supra_client.faucet(alice.address(), wait_for_faucet=False) + assert tx_hash, "The faucet did not accept the request, try again" + print(f"Faucet request accepted, tx_hash: {tx_hash}") + + faucet_transaction = await supra_client.wait_for_faucet(tx_hash) + print(f"Faucet transaction status: {faucet_transaction['status']}") + assert faucet_transaction["status"] == "Success", "The faucet transaction failed" - # Send faucet request + wait for faucet tx execution in single go. - tx_hash = await supra_client.faucet(alice.address()) - assert tx_hash, "faucet request resolution missed, try again" alice_balance = await supra_client.account_supra_balance(alice.address()) - print( - f"Faucet on {alice.address()} successfully done, tx_hash: {tx_hash}, faucet_amount: {alice_balance}" - ) - - # First send faucet request and then manually wait for faucet tx execution. - tx_hash = await supra_client.faucet(bob.address(), wait_for_faucet=False) - assert tx_hash, "faucet request resolution missed, try again" - await supra_client.wait_for_faucet(tx_hash) - bob_balance = await supra_client.account_supra_balance(bob.address()) - print( - f"Faucet on {bob.address()} successfully done, tx_hash: {tx_hash}, faucet_amount: {bob_balance}" - ) + print(f"Alice's balance: {alice_balance}") await supra_client.close() diff --git a/examples/fee_payer_transfer_coin.py b/examples/fee_payer_transfer_coin.py index 915683a..c1aa90d 100644 --- a/examples/fee_payer_transfer_coin.py +++ b/examples/fee_payer_transfer_coin.py @@ -4,7 +4,7 @@ import asyncio -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.bcs import Serializer from supra_sdk.clients.rest import SupraClient @@ -16,7 +16,7 @@ from supra_sdk.type_tag import StructTag, TypeTag -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) alice = Account.generate() bob = Account.generate() @@ -26,9 +26,9 @@ async def main(): print(f"Bob account address: {bob.address()}") print(f"Sponsor account address: {sponsor.address()}") - await supra_client.faucet(sponsor.address()) - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts( + supra_client, [sponsor.address(), alice.address(), bob.address()] + ) print("Accounts data before fee payer based transfer transaction") print( @@ -65,7 +65,7 @@ async def main(): tx_hash = await supra_client.submit_transaction(signed_transaction) print(f"Transaction successfully executed, tx_hash: {tx_hash}") - print("Accounts data before fee payer based transfer transaction") + print("Accounts data after fee payer based transfer transaction") print( f"Alice sequence number: {await supra_client.account_sequence_number(alice.address())}" ) diff --git a/examples/fungible_asset.py b/examples/fungible_asset.py index 3d8bf64..74c1f5f 100644 --- a/examples/fungible_asset.py +++ b/examples/fungible_asset.py @@ -10,19 +10,20 @@ import asyncio -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.clients.rest import SupraClient from supra_sdk.clients.rest.rest_types import PaginationWithOrder -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) alice = Account.generate() bob = Account.generate() - await supra_client.faucet(alice.address()) + # Bob only receives the fungible asset transfer, which creates his primary store. + await fund_accounts(supra_client, [alice.address()]) # `account_supra_balance` sums the CoinStore and the fungible store; `account_fa_balance` reads the latter. print( diff --git a/examples/hello_blockchain.py b/examples/hello_blockchain.py index 1937e9e..303948d 100644 --- a/examples/hello_blockchain.py +++ b/examples/hello_blockchain.py @@ -2,19 +2,21 @@ # Parts of the project are originally copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 -"""This example depends on the hello_blockchain.move module having already been published to the destination blockchain. +"""Sets and reads a message with the `message` module of the `hello_blockchain` package. -Steps: - * `cd ~/aptos-core/aptos-move/move-examples/hello_blockchain` - * `supra move tool publish --named-addresses hello_blockchain=${your_address_from_supra_init}` - * `python3 -m examples.hello_blockchain ${your_address_from_supra_init}` +`python -m examples.hello_blockchain` publishes the compiled package of +`examples/move/packages.json` under Alice's address and uses it. +`python -m examples.hello_blockchain
` uses the package already published at +`
` instead, for example one published with: + + supra move tool publish --named-addresses hello_blockchain=
""" import asyncio import sys from typing import Any -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, compiled_package, fund_accounts from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.bcs import Serializer @@ -58,7 +60,7 @@ async def set_message( return await self.submit_transaction(signed_transaction) -async def main(contract_address: AccountAddress): +async def main(contract_address: AccountAddress | None) -> None: alice = Account.generate() bob = Account.generate() @@ -66,8 +68,14 @@ async def main(contract_address: AccountAddress): print(f"Bob account address: {bob.address()}") supra_client = HelloBlockchainClient(RPC_NODE_URL) - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [alice.address(), bob.address()]) + + if contract_address is None: + print("\n=== Publishing the hello_blockchain package ===") + package = compiled_package("hello_blockchain", alice.address()) + await supra_client.publish_package(alice, package.metadata, package.modules) + contract_address = alice.address() + print(f"Published under {contract_address}") print("\n=== Testing Alice ===") message = await supra_client.get_message(contract_address, alice.address()) @@ -89,7 +97,7 @@ async def main(contract_address: AccountAddress): if __name__ == "__main__": - assert len(sys.argv) == 2, "Expecting the contract address" - contract_address_str = sys.argv[1] - - asyncio.run(main(AccountAddress.from_str(contract_address_str))) + assert len(sys.argv) <= 2, "Expecting at most the contract address" + asyncio.run( + main(AccountAddress.from_str(sys.argv[1]) if len(sys.argv) == 2 else None) + ) diff --git a/examples/move/packages.json b/examples/move/packages.json new file mode 100644 index 0000000..4f70c4a --- /dev/null +++ b/examples/move/packages.json @@ -0,0 +1,36 @@ +{ + "address": "0x9c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e", + "packages": { + "hello_blockchain": { + "metadata": "084578616d706c657301000000000000000040394242393236323139343532384445344343393345464239323230444538314643363845373435453631463130363432363241444345333743323741464636428f011f8b08000000000002ff3dccc10ac2300c06e07b9ea2f46ee70b78d417f038c6886d74a55d5b1aa682f8ee2682925cf2e74bc6863ee18d2628b89239187b7ce2da32b1853b758eb568b877521660c4103a31134fb050ce75bee4ea935f307eddac2650a312a8f8a8ecbcb58ea72edf1fb527412f232798953b37485f7fcb81d5eefeb3356ff800c97afdf5a100000001076d657373616765e7041f8b08000000000002ff9555db6adb40107df7578c132812883a8552ca3a0ebd609a97b40fce5b088b2c8d2c6169d7dd8b1d53f4ef5dad2e5d599693ee8bf1ecec9973e6a682c73a474831cf395de73cda46699831420a9432dc20fc9980395a2248151382427031efdb64b66138302a91b18d63d43b11d24484051eb8d81aa43d32359f5887d98c4cc99d40c9b588d09acc7b1d2978a869dcf33c46016928618bc78654751a9a04ea7884acec6f601dca06fcd6a077e0d676fd64c33f9f09f53d0d99d15d858a05df05e69a0b74428651c4355304c23836a832e86e12c10b7a91517514a7afb06e68cfe03145133a93c078ab147626a6a16e7d22cea482e5cf5ff461b95a7dfdb124a03f7d8405dccc5b9dfb0c0fb5cc9d5ee759048966b041d572f02a159d16ff949251fb5b67e6eaa4124e3aa444a1a61ebe6452c9db9edb9d45f703b06d4308e38a262679b1e770f6fd7907b6e6c6f14037395f87f959acf70d6f37558d32931571b4faa4abaf2d57dda6c158cbf893bab8afc8cd51b51d402b4226d93530214d0e294fbc778d8b232d4bc09b8e24c9c1f37d2798ed70be47aa7807198c523b9989a07751fadddf12309778f2b0d26500dbb4d1b4465ff44b420bad2eb29f0f40dda13070c3186d45fb4fed7c9a2d5164caeb4fe650f0bf8974a804033797c9f0d61dcb88ef8e6369ecd31c9763b40e84956ed75e3f2994aa4d9f71ff72f3f2c17f1eeb6866b06914327aa1b9fd37f6effff4ed6071b7314924305448dba4275cd05a51bf11cef00dbad9d32af9ecadafeeabef4f00dfba0fd055b515063ba697fad325e6c362f106dc7e3d9d3dd4d91beee5a49cfc059c069e131f07000000000400000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200000000000000000000000000000000000000000000000000000000000000010e53757072614672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b53757072615374646c696200", + "modules": [ + "a11ceb0b060000000c01000a020a0c03161a04300205321c074e970108e5014006a5020a10af02670a9603110ca703590d8004020000010101020103010400050600000608000407070000080001000009020300010d040400030e060000020f0803010604070105010802020c08020001030305080207080101060c010800010900076d657373616765056572726f72056576656e74067369676e657206737472696e670d4d6573736167654368616e67650d4d657373616765486f6c64657206537472696e670b6765745f6d6573736167650b7365745f6d657373616765076163636f756e740c66726f6d5f6d6573736167650a746f5f6d657373616765096e6f745f666f756e640a616464726573735f6f6604656d69749c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e000000000000000000000000000000000000000000000000000000000000000103080000000000000000126170746f733a3a6d657461646174615f7631530100000000000000000b454e4f5f4d4553534147451b5468657265206973206e6f206d6573736167652070726573656e74010d4d6573736167654368616e6765010400010b6765745f6d6573736167650101000002030a050b08020c08020102010008020001000101030c0a0029010404050707001102270b002b01100014020101040101051d0e0011030c020a02290120040c0e000b0112012d01051c0a022a010c040a041000140c030b020b030a01120038000b010b040f001502010000" + ], + "scripts": [] + }, + "moon_coin": { + "metadata": "084578616d706c657301000000000000000040323841333042453530464637344636363434463135324432443136343830373535414341433132313736363633414435463846373733463146393438374242328a011f8b08000000000002ff3d8c410a02310c45f73945e9de8e177025ba73e5721824b451ca7492d2302a8877b71194fccdffef91b1629cf14613302ee476ce1f9eb8d442eae14e4db3b08ddbd0cf038c98522355d2094e22bc97fce51763892a71228ed9f079ad0d8fad7f7d489bbbf472452216d343187aae3f38a8b99b7ff7ee0d1f7cde11c59900000001096d6f6f6e5f636f696ebb011f8b08000000000002ff6590c10e82301044effd8acac14062d29b879570f1ac7c02a9b09046d89a96c644c2bfdb56c5a03d4d6767df242b046ca018b42636e8c6f5c84f5e1fb52280e056b5977c62dc3f3b1a578f4b804f338b7eeb882b5263f522a416a941037c6b554768b2f77a44b89b91556be480776daebe4392ecb089350081a264af1e987f5a8a74598e80c8deadbc4b12c23ca493bf49599e7fccfdfadbcadee2d7ca0e51ce6c6642e4fe38f1364f98ca2b372701000000000400000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200000000000000000000000000000000000000000000000000000000000000010e53757072614672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b53757072615374646c696200", + "modules": [ + "a11ceb0b060000000a01000402040403080b04130205151007254308684006a801150abd01050cc2011200000101000200000003000100010503010100010201060c0001080005060c0a020a020201096d6f6f6e5f636f696e0c6d616e616765645f636f696e084d6f6f6e436f696e0b696e69745f6d6f64756c650b64756d6d795f6669656c640a696e697469616c697a659c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e00000000000000000000000000000000000000000000000000000000000000010a020a094d6f6f6e20436f696e0a0205044d4f4f4e00020104010000000001070b000700070131060938000200" + ], + "scripts": [] + }, + "upgrade_and_govern_genesis": { + "metadata": "1055706772616465416e64476f7665726e0100000000000000004031344435454346344444333044363333333745413635394142354541453339373846323341314333313342393930423034393231334542414232304543343433a5011f8b08000000000002ff3d8ec10ac2400c44eff98a951ef6e4b65e8b0a5ef403c4532925346929b5d96563f5f7ed5a2c0c8161de90c94cb92bcf13cad0b1bea00ad88ed8730d82139b93b18fd04724be08ddfc9ba35858ae0e5e527870852b2c4085449155596b98d74283424dffab24b2491471602196766075f73944bcc6e5cdc7c7b186a76ff19950e7f255dd3fcc35b1fbcd5b9399e3b27b9bfd0579d86b25c6000000010a706172616d6574657273da021f8b08000000000002ff8d93516f823010c7dff914b79705122791391f40cd7c20cb1e5c8cfade94529188ad2bed9665e1bb0f01a186bad8b7e67ebdbbffff7aae0bfe833f3ff25865d4725d17969c1c80b327b2c72983847f51c13023144e58e0239554e443abe6419d1281638a308b514dfa7e87c1af65417972291491f0d6a65a75c81ee770a03f250acd69dfa3910f6a321e18225e15a902455d8370969725c28f70f3be41abc57ab10cb7e1bac901331879cfc1bfa47721c72f93a04eba530c52964a54cbb5db4e0cbae1314f134645c5389a9e8c4acd45a4d933335ad2bdecb9619037b8417b26da2b82163f96a591e453530b73fb2a6b5fedc0acc809f4919c5494a5a43231a152c36cc7af00fb3cddb3e94e75c5e453a582e6e68f7287a148d01dccae5a8fb810fc1b25198f707643eb6b5f9fd3f964df2c35d466a38de11ede732e461550aedcb45cc16603ff006ab421ab9003000000000400000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200000000000000000000000000000000000000000000000000000000000000010e53757072614672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b53757072615374646c696200", + "modules": [ + "a11ceb0b060000000a01000202020403060a05100b071b53086e20068e01360ac401070ccb01290df40104000000010800000200010000030200000002030301060c010608000a706172616d657465727314476f7665726e616e6365506172616d65746572730e6765745f706172616d65746572730b696e69745f6d6f64756c650b706172616d657465725f310b706172616d657465725f329c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e03087b000000000000000308c80100000000000005209c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e000202040305030001000100030a07022b000c000a001000140b00100114020100000000060b000700070112002d00020000000100" + ], + "scripts": [] + }, + "upgrade_and_govern_upgrade": { + "metadata": "1055706772616465416e64476f7665726e0100000000000000004036464336354138303334353746453943343345463032363939413532363042364432323034374534334231413438363143464438423533324532344430344139a5011f8b08000000000002ff3d8ec10ac2400c44eff98a951ef6e4d65e8b0a5ef403c4532925346929b5d96563f5f7ed5a2c0c8161de90c94cb92bcf13cad0b1bea00ad88ed8730d82139b93b18fd04724be08ddfc9ba35858ae0e5e5258b8c21d2c4085449155596b98d74283424dffab24b2491471602196766075f73944bcc6e5cdc7c7b186a76ff19950e7f255dd3fcc35b1fbcd5b9399e3b27b9bfd056f2f79dfc6000000020a706172616d6574657273d4031f8b08000000000002ff9d534b4fe33010bef7570c179448854296e5e010442522a8b4b0a814244e969bb82122b5831f20b4ea7f5f2781c451ddaae09b335f66fc3d66341ac10d4f5e80b383e499e40c32fe4605232ca1501241965451210f074b9eea82822e3341528a094b718344a883c1bfc100cc914ae844c155dbeaae833c13092ff4c340e1f3b4ffe36304faf464e8a80475a52eac9a190967d28c886fe3fbc93dbe1b4fc737f12c9e7ef680088e835fe15664f0853cf97d1a364d179a41ce72851bba5efb12076fd89779c6a8a831bec5a7a0ca52115bf2444e49ba3fd7d470d01b6e40072e74b00a5bf8d28cc68a9fb99e70eef5baaeb31dba19f9a16d49a9e7459ed42266545930cf4735c0abdcad44f7eb2b495e752ea87407650741b1a00b887a4f9f7321f83bce0a3e27c506ae17ebfcfc4e276fe3a843cb1bcb865df0816f0b65960eeda173529694a534adbf6949cddea40835a94288a4a9d14662be08edc4c77872fb38fe33b9c4e387d9f5dfe964f6f495e3a3b0e703654a7cd46ec8be1bbba47af8d3f5f477b695484985daf33aa29ec31888227018d65f83ea3884b15cdd9a21bcd46a5b8eaafa8fb2b475a21d27e39e75fb7e83a0d72068d7725565edcc84adcdda7f3cad8871f20500000000087472616e7366657296021f8b08000000000002ff9552b16e84300cddf90a77a9404245309aeb2d3777ea074429188a80843ac9555575ff5e205cb893daa19eece767fbd97296013ee071d4b51b28cab20c5e74d543a53b0596a5320d31f82cd87769a15367dd9381569f89955415c124598e6489cd53b451ddd4b2ac4948550bcf440cedbea308667386c0b889a56896fa4fcd3da20796f188af8b7f9addf26f7e7597fe6decaeaef47327f736741590b2fc058ddbf7141f4e2a6be295b558c37a4478345dab88d3005b2d720459d74cc6dcc1458057349957bd6607b210cb513b65459ec2e615093cdfdc0fb1252bf6384ecad0c0dfe4aaf5108e738c1799e9aa2af4cdff5758dc08f2859728bac0fc0d87f93bb6e7f80153f895ce2b02000000000400000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200000000000000000000000000000000000000000000000000000000000000010e53757072614672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b53757072615374646c696200", + "modules": [ + "a11ceb0b060000000b010004020404030814051c1607327408a6014006e6014010a602340ada02070ce102530db40304000001010002080000030001000004020000000503000001080206000002030301060c03060c0303010608000107080001050a706172616d6574657273067369676e657214476f7665726e616e6365506172616d65746572730e6765745f706172616d65746572730b696e69745f6d6f64756c650e7365745f706172616d65746572730b706172616d657465725f310b706172616d657465725f320a616464726573735f6f669c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e00000000000000000000000000000000000000000000000000000000000000010308000000000000000003087b000000000000000308c80100000000000005209c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e126170746f733a3a6d657461646174615f76312001000000000000000013455f494e56414c49445f415554484f52495459000000000202060307030001000100040a07032b000c000a001000140b00100114020100000000060b000701070212002d0002020104010005140b0011030703210406050807002707032a000c030b010a030f00150b020b030f0115020000000100", + "a11ceb0b0600000008010008020804030c10041c02051e11072f4d087c400cbc011d00000101010200030206080000040001000305010200010004010100020303060c05050002030301080003060c0503087472616e7366657204636f696e0a73757072615f636f696e0a706172616d65746572730f7472616e736665725f7175616e74730e6765745f706172616d6574657273095375707261436f696e9c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e000000000000000000000000000000000000000000000000000000000000000100010400020c11010c040c030a000b010b0338000b000b020b0438000200" + ], + "scripts": [ + "a11ceb0b060000000601000403040a050e0b071933084c20066c14000000010002020100010300010003060c05050003060c03030a706172616d6574657273087472616e736665720e7365745f706172616d65746572730f7472616e736665725f7175616e74739c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e03082c010000000000000308c800000000000000000001090a000700070111000b000b010b02110102" + ] + } + } +} diff --git a/examples/multisig.py b/examples/multisig.py index 0efd794..c90a77d 100644 --- a/examples/multisig.py +++ b/examples/multisig.py @@ -2,13 +2,23 @@ # Parts of the project are originally copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +"""A 2-of-3 MultiEd25519 account: a transfer, a key rotation to the multisig key, a +package publication and upgrade, and a Move script, all signed by the multisig. + +The packages are the `upgrade_and_govern` packages of `examples/move/packages.json`. +`python -m examples.multisig true` pauses between the steps. +""" + import asyncio import sys from typing import cast -import aiofiles - -from examples.common import RPC_NODE_URL, SUPRA_CORE_PATH +from examples.common import ( + QUANTS_PER_SUPRA, + RPC_NODE_URL, + compiled_package, + fund_accounts, +) from supra_sdk import ed25519 from supra_sdk.account import Account, RotationProofChallenge from supra_sdk.account_address import AccountAddress @@ -33,6 +43,13 @@ # 1,700 units) and a package publish (about 2,000 units). MAX_GAS_AMOUNT = 20_000 +# The balance of each account that sends a transaction with MAX_GAS_AMOUNT must cover +# MAX_GAS_AMOUNT at the gas unit price (20 SUPRA at the minimum price of 100,000 quants). +MULTISIG_SENDER_FUNDING_QUANTS = 30 * QUANTS_PER_SUPRA + +# The signers send no transaction; they receive the transfers of the example. +SIGNER_FUNDING_QUANTS = 1 * QUANTS_PER_SUPRA + def wait(): """Wait for user to press Enter before starting next section.""" @@ -40,7 +57,7 @@ def wait(): input("\nPress Enter to continue...") -async def main(should_wait_input: bool): +async def main(should_wait_input: bool) -> None: global should_wait should_wait = should_wait_input @@ -81,10 +98,14 @@ async def main(should_wait_input: bool): wait() print("\n=== Funding accounts ===") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) - await supra_client.faucet(chad.address()) - await supra_client.faucet(multisig_address) + await fund_accounts( + supra_client, + [alice.address(), bob.address(), chad.address()], + SIGNER_FUNDING_QUANTS, + ) + await fund_accounts( + supra_client, [multisig_address], MULTISIG_SENDER_FUNDING_QUANTS + ) alice_balance = await supra_client.account_supra_balance(alice.address()) bob_balance = await supra_client.account_supra_balance(bob.address()) @@ -162,7 +183,9 @@ async def main(should_wait_input: bool): print(f"Deedee's address: {deedee.address()}") print(f"Deedee's public key: {deedee.public_key()}") - await supra_client.faucet(deedee.address()) + await fund_accounts( + supra_client, [deedee.address()], MULTISIG_SENDER_FUNDING_QUANTS + ) deedee_balance = await supra_client.account_supra_balance(deedee.address()) print(f"Deedee's balance: {deedee_balance}") # <:!:section_7 wait() @@ -217,29 +240,9 @@ async def main(should_wait_input: bool): wait() print("\n=== Genesis publication ===") - packages_dir = f"{SUPRA_CORE_PATH}/aptos-move/move-examples/upgrade_and_govern/" - - command = ( - f"supra move tool compile " - f"--save-metadata " - f"--package-dir {packages_dir}genesis " - f"--named-addresses upgrade_and_govern={deedee.address()!s}" - ) - - print(f"Running supra CLI command: {command}\n") - process = await asyncio.create_subprocess_exec( - *command.split(), - ) - await process.wait() - assert process.returncode == 0, "supra move tool compile failed" - - build_path = f"{packages_dir}genesis/build/UpgradeAndGovern/" - - async with aiofiles.open(f"{build_path}package-metadata.bcs", "rb") as f: - package_metadata = await f.read() - - async with aiofiles.open(f"{build_path}bytecode_modules/parameters.mv", "rb") as f: - parameters_module = await f.read() + genesis_package = compiled_package("upgrade_and_govern_genesis", deedee.address()) + package_metadata = genesis_package.metadata + [parameters_module] = genesis_package.modules modules_serializer = Serializer.sequence_serializer(Serializer.to_bytes) @@ -283,27 +286,9 @@ async def main(should_wait_input: bool): wait() print("\n=== Upgrade publication ===") - command = ( - f"supra move tool compile " - f"--save-metadata " - f"--package-dir {packages_dir}upgrade " - f"--named-addresses upgrade_and_govern={deedee.address()!s}" - ) - - print(f"Running supra CLI command: {command}\n") - process = await asyncio.create_subprocess_exec(*command.split()) - await process.wait() - assert process.returncode == 0, "supra move tool compile failed" - build_path = f"{packages_dir}upgrade/build/UpgradeAndGovern/" - - async with aiofiles.open(f"{build_path}package-metadata.bcs", "rb") as f: - package_metadata = await f.read() - - async with aiofiles.open(f"{build_path}bytecode_modules/parameters.mv", "rb") as f: - parameters_module = await f.read() - - async with aiofiles.open(f"{build_path}bytecode_modules/transfer.mv", "rb") as f: - transfer_module = await f.read() + upgrade_package = compiled_package("upgrade_and_govern_upgrade", deedee.address()) + package_metadata = upgrade_package.metadata + [parameters_module, transfer_module] = upgrade_package.modules entry_function_payload = EntryFunction.natural( module="0x1::code", @@ -349,10 +334,7 @@ async def main(should_wait_input: bool): wait() print("\n=== Invoking Move script ===") - async with aiofiles.open( - f"{build_path}bytecode_scripts/set_and_transfer.mv", "rb" - ) as f: - script_code = await f.read() + [script_code] = upgrade_package.scripts script_payload = Script( code=script_code, ty_args=[], diff --git a/examples/rotate_key.py b/examples/rotate_key.py index d6486d2..3990c1e 100644 --- a/examples/rotate_key.py +++ b/examples/rotate_key.py @@ -7,7 +7,7 @@ import supra_sdk.asymmetric_crypto as asymmetric_crypto import supra_sdk.ed25519 as ed25519 -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account, RotationProofChallenge from supra_sdk.account_address import AccountAddress from supra_sdk.authenticator import Authenticator @@ -30,14 +30,15 @@ def format_account_info(account: Account) -> str: vals = [ str(account.address()), account.auth_key(), - account.private_key.hex(), str(account.public_key()), ] return "".join([truncate(v).ljust(WIDTH, " ") for v in vals]) async def rotate_auth_key_ed25519_payload( - supra_client: SupraClient, from_account: Account, to_private_key: ed25519.PrivateKey + supra_client: SupraClient, + from_account: Account, + to_private_key: asymmetric_crypto.PrivateKey, ) -> TransactionPayload: to_account = Account.load_key(to_private_key.hex()) rotation_proof_challenge = RotationProofChallenge( @@ -64,7 +65,7 @@ async def rotate_auth_key_ed25519_payload( async def rotate_auth_key_multi_ed25519_payload( supra_client: SupraClient, from_account: Account, - private_keys: list[ed25519.PrivateKey], + private_keys: list[asymmetric_crypto.PrivateKey], ) -> TransactionPayload: to_accounts = list( map(lambda private_key: Account.load_key(private_key.hex()), private_keys) @@ -125,7 +126,7 @@ def rotation_payload( return TransactionPayload(entry_function) -async def main(): +async def main() -> None: # Initialize the clients used to interact with the blockchain supra_client = SupraClient(RPC_NODE_URL) @@ -134,7 +135,7 @@ async def main(): bob = Account.generate() # Fund Alice's account, since we don't use Bob's - await supra_client.faucet(alice.address()) + await fund_accounts(supra_client, [alice.address()]) # Display formatted account info print( @@ -142,12 +143,9 @@ async def main(): + "Account".ljust(WIDTH, " ") + "Address".ljust(WIDTH, " ") + "Auth Key".ljust(WIDTH, " ") - + "Private Key".ljust(WIDTH, " ") + "Public Key".ljust(WIDTH, " ") ) - print( - "-------------------------------------------------------------------------------------------" - ) + print("-" * (4 * WIDTH)) print("Alice".ljust(WIDTH, " ") + format_account_info(alice)) print("Bob".ljust(WIDTH, " ") + format_account_info(bob)) diff --git a/examples/simple_nft.py b/examples/simple_nft.py index 7969e5f..d2a00e5 100644 --- a/examples/simple_nft.py +++ b/examples/simple_nft.py @@ -5,13 +5,13 @@ import asyncio import json -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.clients import SupraTokenClient from supra_sdk.clients.rest import SupraClient -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) token_client = SupraTokenClient(supra_client) @@ -25,8 +25,7 @@ async def main(): print(f"Alice account address: {alice.address()}") print(f"Bob account address: {bob.address()}") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [alice.address(), bob.address()]) print("\n=== Creating Collection and Token ===") await token_client.create_collection( @@ -77,13 +76,22 @@ async def main(): property_version, ) - alice_balance = token_client.get_token_balance( - alice.address(), alice.address(), collection_name, token_name, property_version - ) - bob_balance = token_client.get_token_balance( - bob.address(), alice.address(), collection_name, token_name, property_version + alice_balance, bob_balance = await asyncio.gather( + token_client.get_token_balance( + alice.address(), + alice.address(), + collection_name, + token_name, + property_version, + ), + token_client.get_token_balance( + bob.address(), + alice.address(), + collection_name, + token_name, + property_version, + ), ) - [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) print(f"Alice's token balance: {alice_balance}") print(f"Bob's token balance: {bob_balance}") @@ -91,13 +99,22 @@ async def main(): await token_client.direct_transfer_token( bob, alice, alice.address(), collection_name, token_name, 0, 1 ) - alice_balance = token_client.get_token_balance( - alice.address(), alice.address(), collection_name, token_name, property_version - ) - bob_balance = token_client.get_token_balance( - bob.address(), alice.address(), collection_name, token_name, property_version + alice_balance, bob_balance = await asyncio.gather( + token_client.get_token_balance( + alice.address(), + alice.address(), + collection_name, + token_name, + property_version, + ), + token_client.get_token_balance( + bob.address(), + alice.address(), + collection_name, + token_name, + property_version, + ), ) - [alice_balance, bob_balance] = await asyncio.gather(*[alice_balance, bob_balance]) print(f"Alice's token balance: {alice_balance}") print(f"Bob's token balance: {bob_balance}") diff --git a/examples/simple_supra_token.py b/examples/simple_supra_token.py index 565d52b..a179f06 100644 --- a/examples/simple_supra_token.py +++ b/examples/simple_supra_token.py @@ -5,7 +5,7 @@ import asyncio import json -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.clients.rest import SupraClient @@ -48,7 +48,7 @@ async def get_token_data( } -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) token_client = SupraTokenObjectClient(supra_client) @@ -62,8 +62,7 @@ async def main(): print(f"Alice account address: {alice.address()}") print(f"Bob account address: {bob.address()}") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [alice.address(), bob.address()]) print("\n=== Creating Collection and Token ===") diff --git a/examples/simulate_transfer_coin.py b/examples/simulate_transfer_coin.py index 7119f77..9d8e314 100644 --- a/examples/simulate_transfer_coin.py +++ b/examples/simulate_transfer_coin.py @@ -5,7 +5,7 @@ import asyncio import json -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.bcs import Serializer from supra_sdk.clients.api_client import ApiError @@ -17,8 +17,14 @@ ) from supra_sdk.type_tag import StructTag, TypeTag +# Max gas of the transfer, which uses fewer than 100 units. +MAX_GAS_AMOUNT = 2_000 -async def main(): +# The abort of `0x1::coin` for a receiver without a coin store. +COIN_STORE_ABORT = "ECOIN_STORE_NOT_PUBLISHED" + + +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) alice = Account.generate() bob = Account.generate() @@ -26,7 +32,7 @@ async def main(): print(f"Alice account address: {alice.address()}") print(f"Bob account address: {bob.address()}") - await supra_client.faucet(alice.address()) + await fund_accounts(supra_client, [alice.address()]) payload = EntryFunction.natural( "0x1::coin", @@ -37,22 +43,36 @@ async def main(): TransactionArgument(100_000, Serializer.u64), ], ) + # The max gas amount is given: without it the SDK estimates it by simulating the + # transfer, and that simulation fails while Bob has no coin store. signed_transaction = await supra_client.create_signed_transaction( - alice, TransactionPayload(payload) + alice, TransactionPayload(payload), max_gas_amount=MAX_GAS_AMOUNT ) print("\n=== Simulate before creating Bob's Account ===") + # Bob has no coin store, so `0x1::coin::transfer` aborts. The node reports the abort + # in the simulation result, or refuses the simulation with an error naming it. try: - await supra_client.simulate_transaction(signed_transaction) + simulation_result = await supra_client.simulate_transaction(signed_transaction) except ApiError as e: - assert "ABORTED" in str(e), "Expected CoinStore error" + print(f"Simulation refused: {e}") + assert "ABORTED" in str(e) or COIN_STORE_ABORT in str(e), ( + f"Expected the coin store abort, got: {e}" + ) + else: + status = simulation_result.get("status") + vm_status = simulation_result.get("output", {}).get("Move", {}).get("vm_status") + print(f"Simulation status: {status}, vm_status: {vm_status}") + assert status == "Fail" and COIN_STORE_ABORT in str(vm_status), ( + f"Expected the coin store abort, got status {status}, vm_status {vm_status}" + ) print("\n=== Simulate after creating Bob's Account ===") - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [bob.address()]) - # Create another transaction with broken signature for second simulation + # The same signed transaction, simulated again now that Bob's account exists. simulation_result = await supra_client.simulate_transaction(signed_transaction) - vm_status = simulation_result["output"]["Move"]["vm_status"] + vm_status = simulation_result.get("output", {}).get("Move", {}).get("vm_status") assert vm_status == "Executed successfully", f"Expected success, got: {vm_status}" print(json.dumps(simulation_result, indent=4, sort_keys=True)) diff --git a/examples/supra_token.py b/examples/supra_token.py index b2f26c3..b22f23f 100644 --- a/examples/supra_token.py +++ b/examples/supra_token.py @@ -4,7 +4,7 @@ import asyncio -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.clients.rest import SupraClient @@ -16,7 +16,7 @@ ) -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) token_client = SupraTokenObjectClient(supra_client) @@ -29,8 +29,7 @@ async def main(): print(f"Alice account address: {alice.address()}") print(f"Bob account address: {bob.address()}") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [alice.address(), bob.address()]) print("\n=== Creating Collection and Token ===") @@ -107,7 +106,6 @@ async def main(): print(f"Owner: {token_data.resources[Object].owner}\n") await supra_client.close() - await supra_client.close() if __name__ == "__main__": diff --git a/examples/transfer_coin.py b/examples/transfer_coin.py index 3971d3b..5e3b68f 100644 --- a/examples/transfer_coin.py +++ b/examples/transfer_coin.py @@ -4,12 +4,12 @@ import asyncio -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.clients.rest import SupraClient -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) alice = Account.generate() @@ -18,8 +18,7 @@ async def main(): print(f"Alice account address: {alice.address()}") print(f"Bob account address: {bob.address()}") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) + await fund_accounts(supra_client, [alice.address(), bob.address()]) print("\n=== Initial Balances ===") alice_balance = await supra_client.account_supra_balance(alice.address()) @@ -36,7 +35,7 @@ async def main(): print(f"Alice: {alice_balance}") print(f"Bob: {bob_balance}") - # Have Alice give Bob another 1_000 coins using BCS + # Have Alice give Bob another 1_000 coins await supra_client.transfer_supra_coin(alice, bob.address(), 1_000) print("\n=== Final Balances ===") @@ -46,7 +45,6 @@ async def main(): print(f"Bob: {bob_balance}") await supra_client.close() - await supra_client.close() if __name__ == "__main__": diff --git a/examples/transfer_two_by_two.py b/examples/transfer_two_by_two.py index 327983c..9d8ba79 100644 --- a/examples/transfer_two_by_two.py +++ b/examples/transfer_two_by_two.py @@ -3,17 +3,17 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio -import os +from pathlib import Path -import aiofiles - -from examples.common import RPC_NODE_URL +from examples.common import RPC_NODE_URL, fund_accounts from supra_sdk.account import Account from supra_sdk.clients.rest import SupraClient from supra_sdk.transactions import Script, ScriptArgument, TransactionPayload +SCRIPT_PATH = Path(__file__).parent / "two_by_two_transfer.mv" + -async def main(): +async def main() -> None: supra_client = SupraClient(RPC_NODE_URL) alice = Account.generate() @@ -26,10 +26,10 @@ async def main(): print(f"Carol account address: {carol.address()}") print(f"David account address: {david.address()}") - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) - await supra_client.faucet(carol.address()) - await supra_client.faucet(david.address()) + await fund_accounts( + supra_client, + [alice.address(), bob.address(), carol.address(), david.address()], + ) alice_balance = await supra_client.account_supra_balance(alice.address()) bob_balance = await supra_client.account_supra_balance(bob.address()) @@ -42,10 +42,7 @@ async def main(): print(f"Carol: {carol_balance}") print(f"David: {david_balance}") - path = os.path.dirname(__file__) - filepath = os.path.join(path, "two_by_two_transfer.mv") - async with aiofiles.open(filepath, mode="rb") as file: - code = await file.read() + code = SCRIPT_PATH.read_bytes() script_arguments = [ ScriptArgument(ScriptArgument.U64, 100), @@ -73,7 +70,6 @@ async def main(): print(f"David: {david_balance}") await supra_client.close() - await supra_client.close() if __name__ == "__main__": diff --git a/examples/your_coin.py b/examples/your_coin.py index 81a146e..26c60a7 100644 --- a/examples/your_coin.py +++ b/examples/your_coin.py @@ -2,20 +2,15 @@ # Parts of the project are originally copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 -"""This example depends on the MoonCoin.move module. +"""Publishes the `moon_coin` package, a managed coin, and mints and transfers the coin. -Steps: - * `cd ~/aptos-core/aptos-move/move-examples/moon_coin` - * `supra move tool compile --save-metadata --named-addresses MoonCoin=` - * `python3 -m examples.your_coin ` +The package is the compiled `moon_coin` package of `examples/move/packages.json`, +published under Alice's address. """ import asyncio -import os -import aiofiles - -from examples.common import RPC_NODE_URL, SUPRA_CORE_PATH +from examples.common import RPC_NODE_URL, compiled_package, fund_accounts from supra_sdk.account import Account from supra_sdk.account_address import AccountAddress from supra_sdk.bcs import Serializer @@ -71,7 +66,7 @@ async def get_balance( return str(await self.account_coin_balance(account_address, coin_type)) -async def main(): +async def main() -> None: alice = Account.generate() bob = Account.generate() @@ -80,35 +75,12 @@ async def main(): supra_client = CoinClient(RPC_NODE_URL) - await supra_client.faucet(alice.address()) - await supra_client.faucet(bob.address()) - - moon_coin_path = f"{SUPRA_CORE_PATH}/aptos-move/move-examples/moon_coin/" - command = ( - f"supra move tool compile " - f"--save-metadata " - f"--package-dir {moon_coin_path} " - f"--named-addresses MoonCoin={alice.address()!s}" - ) - print(f"Running supra CLI command: {command}\n") - process = await asyncio.create_subprocess_exec(*command.split()) - await process.wait() - assert process.returncode == 0, "supra move tool compile failed" + await fund_accounts(supra_client, [alice.address(), bob.address()]) - module_path = os.path.join( - moon_coin_path, "build", "Examples", "bytecode_modules", "moon_coin.mv" - ) - async with aiofiles.open(module_path, "rb") as f: - module = await f.read() - - metadata_path = os.path.join( - moon_coin_path, "build", "Examples", "package-metadata.bcs" - ) - async with aiofiles.open(metadata_path, "rb") as f: - metadata = await f.read() + package = compiled_package("moon_coin", alice.address()) print("\nPublishing MoonCoin package.") - await supra_client.publish_package(alice, metadata, [module]) + await supra_client.publish_package(alice, package.metadata, package.modules) print("\nBob registers the newly created coin so he can receive it from Alice.") await supra_client.register_coin(alice.address(), bob) @@ -133,6 +105,8 @@ async def main(): balance = await supra_client.get_balance(alice.address(), bob.address()) print(f"Bob's updated MoonCoin balance: {balance}") + await supra_client.close() + if __name__ == "__main__": asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index bcaa8f8..a2e83f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,6 @@ dependencies = [ "typing-extensions>=4.4.0,<5.0.0", ] -[project.optional-dependencies] -examples = ["aiofiles>=24.1.0"] - [project.urls] Homepage = "https://github.com/Entropy-Foundation/supra-python-sdk" Documentation = "https://supra-python-sdk.docs.supra.com/" @@ -81,7 +78,7 @@ docstring-code-format = true [tool.mypy] python_version = "3.12" -files = ["supra_sdk"] +files = ["supra_sdk", "examples"] warn_unused_ignores = true no_implicit_optional = true @@ -98,7 +95,6 @@ module-root = "" [dependency-groups] dev = [ - "aiofiles>=24.1.0", "behave>=1.2.6,<2.0.0", "black>=25.1.0", "coverage>=7.10.6", diff --git a/uv.lock b/uv.lock index 0bb9f25..268db1f 100644 --- a/uv.lock +++ b/uv.lock @@ -7,15 +7,6 @@ resolution-markers = [ "python_full_version < '3.13'", ] -[[package]] -name = "aiofiles" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, -] - [[package]] name = "alabaster" version = "1.0.0" @@ -1297,14 +1288,8 @@ dependencies = [ { name = "typing-extensions" }, ] -[package.optional-dependencies] -examples = [ - { name = "aiofiles" }, -] - [package.dev-dependencies] dev = [ - { name = "aiofiles" }, { name = "behave" }, { name = "black" }, { name = "coverage" }, @@ -1320,16 +1305,13 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aiofiles", marker = "extra == 'examples'", specifier = ">=24.1.0" }, { name = "httpx", extras = ["http2"], specifier = ">=0.28.1,<0.29.0" }, { name = "pynacl", specifier = ">=1.6.2,<2.0.0" }, { name = "typing-extensions", specifier = ">=4.4.0,<5.0.0" }, ] -provides-extras = ["examples"] [package.metadata.requires-dev] dev = [ - { name = "aiofiles", specifier = ">=24.1.0" }, { name = "behave", specifier = ">=1.2.6,<2.0.0" }, { name = "black", specifier = ">=25.1.0" }, { name = "coverage", specifier = ">=7.10.6" },