Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c8f88a3
Document odra_test::odra_env and casper_env
kubaplas Sep 16, 2026
edc8de4
Document that external_contract keeps the trait
kubaplas Sep 16, 2026
c6ab0f7
Migration guide: compile-time changes in 3.0.0
kubaplas Sep 16, 2026
d58c3de
Migration guide: Erc20 mint/burn are no longer entry points
kubaplas Sep 16, 2026
ab0d290
Livenet docs: rate limits, retries and read errors
kubaplas Sep 17, 2026
9648f24
Duration-based block time API (#589)
kubaplas Sep 17, 2026
81a542a
Show the Duration import in the updated tutorial snippets
kubaplas Sep 17, 2026
2a63f85
Native token: contract-to-contract transfers with ContractRef::with_t…
kubaplas Sep 17, 2026
1e09338
Document ContractEnv::debug and the test-support feature (#616)
kubaplas Sep 17, 2026
7d59f87
debug docs: correct what happens on a real network
kubaplas Sep 17, 2026
df6837c
odra-cli tutorial: loading contracts from the contracts file (#566)
kubaplas Sep 17, 2026
b72b16a
Pinned reads: ODRA_CASPER_LIVENET_STATE_ROOT_HASH and odra-cli --stat…
kubaplas Sep 17, 2026
3f46a36
module name also names the package hash key (#385)
kubaplas Sep 17, 2026
aa7c32a
Migration guide: upgrades and the renamed package key
kubaplas Sep 17, 2026
2a8a1cc
Upgrade tutorial: what happens to entry points and storage (#378)
kubaplas Sep 17, 2026
1625262
Docs: livenet examples live in the examples' odra_cli (#631)
kubaplas Sep 17, 2026
b30c718
Document ContractEnv::call_stack / nth_caller and HostEnv snapshots
kubaplas Sep 17, 2026
aeff804
Document HostEnv::concurrently and the async CasperClient API
kubaplas Sep 17, 2026
e85b434
Document #[odra(offchain)]
kubaplas Sep 17, 2026
f74ea21
Document native events on livenet
kubaplas Sep 17, 2026
c3b903f
Document contracts from other crates in Odra.toml
kubaplas Sep 18, 2026
c0cf70c
Document #[odra::ref_helpers] (odra#490)
kubaplas Oct 5, 2026
561bc54
Target Odra 2.10.0: own migration guide, AE-only 3.0.0 guide
kubaplas Oct 6, 2026
5cfd406
Document the dapp registry modules
kubaplas Oct 7, 2026
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
42 changes: 42 additions & 0 deletions docusaurus/docs/advanced/03-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,48 @@ mod test {
}
```

## Offchain

Some functions are useful to have on a contract but should never be entry points: iterating a whole
list, aggregating many balances, building a report. On chain they would cost too much gas or not fit
in a transaction at all. `#[odra(offchain)]` keeps such a function in the module, but out of the
deployed contract: it is not a wasm entry point and not in the schema. It runs on the host instead,
reading the contract's state, so it costs nothing and has no size limit.

### Example

```rust title="examples/src/features/offchain.rs"
#[odra::module]
impl BalanceBook {
pub fn deposit(&mut self, amount: U256) { /* .. */ }

pub fn balance_of(&self, owner: &Address) -> U256 {
self.balances.get_or_default(owner)
}

/// Every holder with its balance: a loop over the whole list, fine on the host.
#[odra(offchain)]
pub fn all_balances(&self) -> Vec<(Address, U256)> {
self.holders
.iter()
.map(|holder| (holder, self.balance_of(&holder)))
.collect()
}
}
```

The function is on the `HostRef` like any getter (`book.all_balances()`, `book.try_all_balances()`),
so tests, deploy scripts and scenarios call it the usual way. OdraVM runs it directly, CasperVM runs
it on the host against the VM's storage, and livenet executes it offline against the chain state,
exactly as it does with every non-mutating entry point. odra-cli lists it under `contract <Name>`
marked as offchain.

The rules: it takes `&self` (it cannot change the state, emit events or transfer tokens), it lives in
a plain `impl` block of the module (not in a trait impl, since the trait is also implemented by the
`ContractRef`), and it cannot be `payable` or `non_reentrant`. It can call the contract's own
functions and non-mutating entry points of other contracts. Other contracts cannot call it: it does
not exist on chain, so it is not on the `ContractRef`.

## Mixing attributes

A function can accept more than one attribute, with one exception: a constructor cannot be payable.
Expand Down
6 changes: 3 additions & 3 deletions docusaurus/docs/advanced/08-delegating-cspr.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ It is possible to test the delegation and undelegation of tokens in the contract
You can see, that we use the new methods from HostEnv, namely:

```rust
fn advance_with_auctions(&self, time_diff: u64);
fn auction_delay(&self) -> u64;
fn unbonding_delay(&self) -> u64;
fn advance_with_auctions(&self, time_diff: impl BlockTimeDiff); // Duration or milliseconds
fn auction_delay(&self) -> u64; // milliseconds
fn unbonding_delay(&self) -> u64; // milliseconds
fn delegated_amount(&self, delegator: Address, validator: PublicKey) -> U512;
```

Expand Down
2 changes: 2 additions & 0 deletions docusaurus/docs/backends/03-casper.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ When deploying a new contract you can pass some arguments to it.
Every contract written in Odra expects those arguments to be set:

- `odra_cfg_package_hash_key_name` - `String` type. The key under which the package hash of the contract will be stored.
Odra's own deployers use `<name>_package_hash`, where `name` is the `name` argument of
`#[odra::module(name = "..")]` or, without one, the name of the module struct.
- `odra_cfg_allow_key_override` - `Bool` type. If `true` and the key specified in `odra_cfg_package_hash_key_name` already exists, it will be overwritten.
- `odra_cfg_is_upgradable` - `Bool` type. If `true`, the contract will be deployed as upgradable.
- `odra_cfg_is_upgrade` - `Bool` type. If `true`, the contract will be upgraded. If we want to install a contract to should be set to `false`.
Expand Down
105 changes: 100 additions & 5 deletions docusaurus/docs/backends/04-livenet.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,20 @@ ODRA_CASPER_LIVENET_EVENTS_URL=<events url>

# Optionally, you can set the gas price tolerance for the transactions. Default is 1.
# ODRA_CASPER_LIVENET_GAS_PRICE_TOLERANCE=

# Optionally, pin every read to a past state root hash (hex). Transactions are refused while set.
# ODRA_CASPER_LIVENET_STATE_ROOT_HASH=
```

:::note
CSPR.cloud is a service that provides mainnet and testnet Casper nodes on demand.
:::

With the proper value in place, we can write our tests or deploy scenarios. In the examples, we can find
a simple binary that deploys a contract and calls it. The test is located in the [erc20_on_livenet.rs] file.
Let's go through the code:
With the proper value in place, we can write our tests or deploy scenarios. The smallest possible
program deploys a contract and calls it - the code below is that program. (In the Odra repository
the same steps live in the examples' [Odra CLI](../tutorials/odra-cli.md) as the `erc20-transfer`
scenario: `cargo run --bin odra_cli -- scenario erc20-transfer --amount 1000`.) Let's go through
the code:

```rust
//! Deploys an ERC20 contract and transfers some tokens to another address.
Expand Down Expand Up @@ -226,6 +231,36 @@ before it is sent to the node, which is the quickest way to see what was actuall
ODRA_LOG_LEVEL=debug cargo run --bin erc20_on_livenet --features=livenet
```

### Reading the past

Set `ODRA_CASPER_LIVENET_STATE_ROOT_HASH` to a state root hash and every read - getters, balances,
`HostEnv::get_named_value` and friends - is answered from the chain state as of that root instead
of the latest one. Transactions are refused while the variable is set, because they would execute
at the chain tip and never show up in the pinned view. State root hashes come from
`chain_get_state_root_hash` (`casper-client get-state-root-hash --block-identifier <height>`), or
from the `status` command of an [Odra CLI](../tutorials/odra-cli.md), which also exposes this as
the `--state-root-hash` flag.

### Rate limits and read errors

Nodes and sidecars rate-limit JSON-RPC calls (NCTL and cspr.cloud both do). A burst of getter
calls - a loop over `token.balance_of(..)` for many accounts, say - can be answered with HTTP 429
or a "request was throttled by the node" error. The Livenet backend retries such reads with an
exponential backoff (five attempts, from 200 ms up to about 3 s in total) before giving up, and
logs each retry at `debug` level.

Every read costs the backend RPC calls: getters run your contract code locally and each storage
access is a query to the node. The backend caches the state root hash for up to five seconds (and
drops it after every transaction it sends) and reuses query responses read at that state root, so
repeated reads and the bookkeeping around each call are mostly free - but keep the number of
distinct reads in mind when a script talks to a public node.

When the node cannot be asked at all - it is unreachable, or still throttled after the retries -
the backend stops with the real reason (`Livenet: reading <field> of <contract> failed: ...`)
rather than reporting a missing value, because your contract code would otherwise mistake the
failure for "not set". Run with `ODRA_LOG_LEVEL=warn` or above to see failed queries that were
recovered from.

### Handling a missing configuration

`odra_casper_livenet_env::env()` panics if any of the required variables is missing or if the
Expand All @@ -251,6 +286,11 @@ To run the above code, we simply need to run the binary with the `livenet` featu
cargo run --bin erc20_on_livenet --features=livenet
```

For anything beyond a one-off script, register the contracts with an [Odra CLI](../tutorials/odra-cli.md)
instead: it keeps the deployed addresses in `resources/<chain>-contracts.toml`, exposes every entry point
as a command and turns scripts like the one above into named scenarios. The examples in the Odra
repository are organised that way.

:::note
Before executing the binary, make sure you built the wasm file - the Livenet backend deploys the
artifact from `wasm/`, and fails with `Failed to find wasm file` if it is missing:
Expand Down Expand Up @@ -302,6 +342,61 @@ Basically, if the entrypoint function is not mutable or does not make a call to
node is used for the state query only. However, the Livenet needs to know the connection between the contracts
and the code, so make sure to deploy or load already deployed contracts

## Native events

Native events emitted by the transactions this environment sends are readable the usual way
(`native_events_count`, `get_native_event`, `last_call().emitted_native_events`). Casper keeps a
message's payload only in the execution result of its transaction, so events emitted earlier, or by
someone else, are not visible; see [Events](../basics/09-events.md#native-events-on-livenet).

## Doing several things at once

Every transaction waits for its block and every read is a round trip to the node, so a script that
deploys five contracts or reads fifty balances spends most of its time waiting. `HostEnv::concurrently`
runs one closure per item, spread over a few worker threads, each with its own node connection and its
own `HostEnv` (same caller and gas as yours), and returns the results in the order of the items:

```rust title="examples/bin/odra_cli.rs"
env.set_gas(cspr!(450));
let addresses = env.concurrently((0..count).collect(), |env, i| {
let mut args = erc20_args();
args.name = format!("Plascoin {i}");
Erc20::deploy(env, args).address()
});
let tokens: Vec<Erc20HostRef> = addresses.iter().map(|a| Erc20::load(env, *a)).collect();

let supplies = env.concurrently(addresses, |env, address| {
Erc20::load(env, address).total_supply()
});
```

The closure gets the environment to use; it cannot capture yours (a `HostEnv` cannot be sent to
another thread, the compiler says so). Deploy inside, return the address, and `load` it in your own
environment. The same code runs on OdraVM and CasperVM, where the items simply run one after another,
so a test written this way needs no livenet.

`cargo run --bin odra_cli --features livenet -- scenario concurrent` runs this against your node and
prints how long the reads take one after another and concurrently.

## Async code

The livenet `HostEnv` is synchronous, like every other backend. Underneath, `CasperClient` in
`odra-casper-rpc-client` is async: every network call exists twice, `xxx` (blocking) and `xxx_async`.
An async program (a web service, a `#[tokio::main]` tool) can use the client directly and run several
calls at once:

```rust
let client = CasperClient::new(CasperClientConfiguration::from_env()?);
let balances = futures::future::join_all(
accounts.iter().map(|account| client.get_balance_async(account))
).await;
```

The blocking calls drive the async ones on a process-wide Tokio runtime. They also work inside a
multi-thread Tokio runtime, which is what `#[tokio::main]` gives you; inside a current-thread runtime
they refuse to run (blocking there would stall every other task), so use the async flavour or
`tokio::task::spawn_blocking`.

## Multiple environments

It is possible to have multiple environments for the Livenet backend. This is useful if we want to easily switch between multiple accounts,
Expand All @@ -313,10 +408,10 @@ has to be used first. If your `integration.env` file has a value that IS present
override the value from the `.env` file.

```bash
ODRA_CASPER_LIVENET_ENV=integration cargo run --bin erc20_on_livenet --features=livenet
ODRA_CASPER_LIVENET_ENV=integration cargo run --bin odra_cli --features=livenet -- deploy
```

To sum up - this command will firstly load the `integration.env` file and then load the missing values from `.env` file.

[.env.sample]: https://github.com/odradev/odra/blob/release/2.9.0/examples/.env.sample
[erc20_on_livenet.rs]: https://github.com/odradev/odra/blob/release/2.9.0/examples/bin/erc20_on_livenet.rs
[odra_cli.rs]: https://github.com/odradev/odra/blob/release/2.10.0/examples/bin/odra_cli.rs
21 changes: 21 additions & 0 deletions docusaurus/docs/basics/03-odra-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ fqn = "flipper::Flipper"
fqn = "counter::Counter"
```

## Contracts from other crates

A contract does not have to live in your project. If the `fqn` starts with the name of a crate your
project depends on, Odra builds that crate's contract for you. For example, with `odra-modules` in
your `Cargo.toml`:

```toml
[[contracts]]
fqn = "odra_modules::erc20::Erc20"

[[contracts]]
fqn = "odra_modules::cep18_token::Cep18"
```

`cargo odra build -c Erc20` and `cargo odra schema -c Erc20` write `wasm/Erc20.wasm` and
`resources/casper_contract_schemas/erc20_schema.json` in the current project, next to the wasm files
and schemas of your own contracts - there is nothing to copy from the other crate's directory.

The contract is then used like any local one: the `Erc20HostRef` comes from `odra_modules::erc20`,
and `env.deploy` in tests or a livenet script finds the wasm by the struct name as usual.

## What's next
In the next section, we'll take a closer look at the code that was generated by Odra by default - the famous
`Flipper` contract.
52 changes: 52 additions & 0 deletions docusaurus/docs/basics/06-communicating-with-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,58 @@ In this example, we use two of them:
* `get_block_time()` - returns the current block time as u64.
* `caller()` - returns an Odra `Address` of the caller (this can be an external caller or another contract).

## Call stack

`caller()` is the address right behind the contract: an account, or another contract that called it.
Sometimes that is not enough. A token with a dedicated burner contract wants to burn from the
*account* that called the burner, not from the burner itself. `self.env().call_stack()` returns
every address on the way, from the account that sent the transaction to the current contract, and
`self.env().nth_caller(n)` walks it: `nth_caller(0)` is `caller()`, `nth_caller(1)` is the caller
of the caller, and so on, `None` when the stack is not that deep.

```rust title="examples/src/features/call_stack.rs"
#[odra::module]
impl CallStackProbe {
/// Returns the immediate caller, the caller of the caller (if any) and the call stack from
/// the initiating account to this contract.
pub fn inspect(&self) -> (Address, Option<Address>, Vec<Address>) {
let env = self.env();
(env.caller(), env.nth_caller(1), env.call_stack())
}
}
```

Called by an account directly, `inspect` answers `(account, None, [account, probe])`. Called
through another contract, it answers `(relay, Some(account), [account, relay, probe])`. The same on
OdraVM, CasperVM and livenet.

:::caution
Trusting an address further up the stack has the usual `tx.origin` caveats: a user can be tricked
into calling a malicious contract that then calls yours. Check the immediate caller first, as the
burner example does, and only then look behind it.
:::

## Debug output

`self.env().debug(message)` prints a message on the host that runs the contract - the quickest way to
see what a contract does in a test:

```rust
self.env().debug(format!("transfer of {amount} from {from:?}"));
```

On OdraVM it always prints. For the Casper VM the contract has to be built with the `test-support`
feature of `odra`, which compiles the call into Casper's `casper_print` host function:

```toml title="Cargo.toml"
odra = { version = "2.10.0", features = ["test-support"] }
```

Run the tests with `cargo odra test -b casper -- --nocapture` to see the output. Without the feature
the call compiles to nothing (the message arguments are still evaluated). Build production wasm
without `test-support`: on a real network the messages would only end up in the node's log, at the
cost of gas for every call.

:::info
You will learn more functions that Odra exposes from host and types it uses in further articles.
:::
Expand Down
62 changes: 61 additions & 1 deletion docusaurus/docs/basics/07-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,77 @@ the function we are calling inside the contract.
- `fn balance_of<T: Addressable>(&self, addr: &T) -> U512` - returns the balance of the account associated with the given address
- `fn block_time(&self) -> u64` - returns the current value of `block_time` in milliseconds, alias: `block_time_millis`
- `fn block_time_secs(&self) -> u64` - retuns the current value of `block_time` in seconds
- `fn advance_block_time(&self, time_diff: u64)` - increases the current value of `block_time` by `time_diff` in milliseconds
- `fn advance_block_time(&self, time_diff: impl BlockTimeDiff)` - increases the current value of `block_time` by
`time_diff`: a `core::time::Duration` or a number of milliseconds (block time has millisecond resolution)
- `fn get_account(&self, n: usize) -> Address` - returns an n-th address that was prepared for you by Odra in advance;
by default, you start with the 0-th account
- `fn emitted_event<T: ToBytes + EventInstance, R: Addressable>(&self, contract_address: &R, event: T) -> bool` - verifies if the event was emitted by the contract
- `fn take_snapshot(&self)` / `fn restore_snapshot(&self)` - remember the state of the test VM and bring
it back, see [Snapshots](#snapshots) below
- `fn concurrently<T, R>(&self, items: Vec<T>, f: impl Fn(&HostEnv, T) -> R) -> Vec<R>` - runs `f` once
per item; one after another here, on worker threads on [livenet](../backends/04-livenet.md#doing-several-things-at-once)
- `fn enable_addressable_entity(&self) -> bool` - switches the backend from legacy mode to
addressable-entity mode, migrating the chain state like a real network upgrade would; returns
`false` if the backend does not support the switch or already runs in that mode (see the
[v3.0.0 migration guide](../migrations/to-3.0.0.md))

Full list of functions can be found in the [`HostEnv`] documentation.

## Snapshots

A test often needs one expensive setup (deploy a few contracts, mint, approve, wire them together)
and then several scenarios branching off it. Instead of repeating the setup, take a snapshot of
the VM and go back to it:

```rust title="examples/src/features/testing.rs"
let env = odra_test::env();
let token = OwnedToken::deploy(&env, /* .. */);
// ... configure the contracts ...

env.take_snapshot();

// Scenario A
token.transfer(&alice, &U256::from(100));
env.advance_block_time(Duration::from_secs(60 * 60));
assert_eq!(token.balance_of(&alice), U256::from(100));

// Back to the starting point: scenario A never happened.
env.restore_snapshot();
assert_eq!(token.balance_of(&alice), U256::zero());

// Scenario B starts from the same point; the snapshot can be restored again and again.
```

A snapshot covers the contract storage, CSPR balances, events and the block time. Only the last
snapshot is kept: taking a new one replaces it. The caller chosen with `set_caller` and the gas
report are not part of it. Snapshots work on OdraVM and on CasperVM (where restoring is only a
pointer back to an older state root); on livenet the state lives on a real chain, so
`take_snapshot` panics there.

## Choosing the backend

`odra_test::env()` returns the backend selected by the `ODRA_BACKEND` environment variable:
`cargo odra test` runs your tests on OdraVM, `cargo odra test -b casper` sets the variable and runs
them against the Casper execution engine, using the wasm files built from the contracts listed in
`Odra.toml`.

Sometimes a test should not follow that switch. A typical case is a module that is used only as a
building block of other contracts and is not registered in `Odra.toml` - there is no wasm file for
it, so under `-b casper` `deploy` would fail. Such a test can be pinned to OdraVM with
`odra_test::odra_env()`:

```rust title="examples/src/features/testing.rs"
#[test]
fn odra_vm_only() {
let test_env = odra_test::odra_env();
let owner = test_env.get_account(0);
let ownable = Ownable::deploy(&test_env, OwnableInitArgs { owner });
assert_eq!(ownable.get_owner(), owner);
}
```

`odra_test::casper_env()` does the opposite and always uses the Casper backend.

## What's next
We take a look at how Odra handles errors!

Expand Down
Loading
Loading