Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/examples.yaml
Original file line number Diff line number Diff line change
@@ -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
35 changes: 24 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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=<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
Expand Down
127 changes: 115 additions & 12 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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.<example_filename_without_py_extension>
```

Running `transfer_coin.py` example:
For example:

```bash
# Devnet, accounts funded from a funded account
SUPRA_EXAMPLES_PRIVATE_KEY=<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=<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 <address>` uses a package already published at `<address>` |
| [`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 <source directory> \
--named-addresses <named address>=0x9c1f5e3a7b2d4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d2e4f6a8b0c1d3e
```

The outputs are `build/<package name>/package-metadata.bcs`, `build/<package name>/bytecode_modules/*.mv` and
`build/<package name>/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`.
69 changes: 48 additions & 21 deletions examples/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand All @@ -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()
Expand Down
Loading
Loading