From 97bb7a0f8a194dcd70c564fdc3e0bc8d939c227a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20P=C5=82askonka?= Date: Wed, 16 Sep 2026 11:31:00 +0200 Subject: [PATCH 1/2] Document the storage layout schema and the odra-cli storage command (odradev/odra#372) Co-Authored-By: Claude Fable 5.1 --- docusaurus/docs/advanced/04-storage-layout.md | 44 +++++++++++++ docusaurus/docs/tutorials/odra-cli.md | 64 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/docusaurus/docs/advanced/04-storage-layout.md b/docusaurus/docs/advanced/04-storage-layout.md index 24937c23a..5e1c461b5 100644 --- a/docusaurus/docs/advanced/04-storage-layout.md +++ b/docusaurus/docs/advanced/04-storage-layout.md @@ -105,3 +105,47 @@ encoded in hexadecimal format. Before being stored in the storage, each value is serialized into bytes using the `CLType` serialization method and subsequently encapsulated with Casper's `Bytes` types. + +## Reading the storage from the outside +You do not have to compute the keys by hand. Every module gets a generated +`odra::schema::SchemaStorageLayout` implementation that describes the layout +shown above as data: the index of each field, what is stored under it and the +types involved. `odra::schema::resolve_storage` walks that description for a +dotted field path (e.g. `borrowers.balances`) and the mapping keys, and returns +the storage location together with the type of the value: + +```rust +use odra::casper_types::bytesrepr::ToBytes; +use odra::schema::{resolve_storage, SchemaStorageLayout, StorageLocation}; + +let layout = Loans::storage_kind(); +let key = account.to_bytes().unwrap(); +let query = resolve_storage(&layout, "borrowers.balances", &[key]).unwrap(); + +if let StorageLocation::State { key } = &query.location { + // `key` is the `state` dictionary item key - read it with the node RPC, + // or directly through the host environment: + let bytes = env.get_storage_value(&contract_address, key.as_bytes()); +} +``` + +`HostEnv` exposes `get_storage_value`, `get_named_value` and +`get_dictionary_value` for the three kinds of locations, and they work on +every backend - OdraVM, CasperVM and livenet. + +The [Odra CLI](../tutorials/odra-cli#storage-command) wraps all of this in the +`storage` command, which prints the layout of a contract and reads any field +by its path. + +Modules that store data outside of the layout described in this article, for +example under named keys, declare their layout explicitly with the `layout` +argument of the module attribute: + +```rust +#[odra::module(layout = odra::schema::StorageKind::named_key::("decimals"))] +pub struct Decimals; +``` + +The named-key storage macros (`single_value_storage!`, `key_value_storage!`, +`base64_encoded_key_value_storage!` and `compound_key_value_storage!`) do this +for you. diff --git a/docusaurus/docs/tutorials/odra-cli.md b/docusaurus/docs/tutorials/odra-cli.md index cdc3b6f3d..c4b289a7b 100644 --- a/docusaurus/docs/tutorials/odra-cli.md +++ b/docusaurus/docs/tutorials/odra-cli.md @@ -700,6 +700,70 @@ Types are shown as the CLI value hints you would type on the command line, not a Running `inspect` without a contract name describes every registered contract. Combined with `--json`, this is a convenient way to feed the contract interface to other tooling. +### Storage command + +The `storage` command reads the state of a deployed contract straight from the storage, without +calling any entry point. It is handy when a value has no getter, or when you want to see exactly +what is stored and where. + +Run it with a contract name only to print the storage layout - every field with its index in the +module and the kind of storage behind it: + +```bash +cargo run --bin odra_cli -- storage DogContract +``` + +```bash +💁 INFO : Storage layout of DogContract (DogContract): +💁 INFO : barks [1]: true|false +💁 INFO : weight [2]: UINT +💁 INFO : name [3]: TEXT +💁 INFO : walks [4]: UINT (repeatable) +``` + +Submodules are printed as nested trees, so the path of any field is the dotted chain of names +down the tree. Pass such a path to read the value: + +```bash +cargo run --bin odra_cli -- storage DogContract name +``` + +```bash +💁 INFO : Contract: DogContract +💁 INFO : Path: name +💁 INFO : Type: TEXT +💁 INFO : Location: state[8a1f3c...] +💁 INFO : Value: "Mantus" +``` + +`Location` tells where the value lives: an item of the Odra `state` dictionary (with its key), a +named key, or an item of a named dictionary. That is exactly what you would query with the node +RPC or a block explorer. + +Whenever the path goes through a `Mapping`, a `List` item or a dictionary, provide the key with +`--key` (or `-k`), using the same value syntax as for entry point arguments. Repeat it for nested +containers, in path order. For an [OwnedToken](../examples/using-odra-modules) that composes +`Ownable` and `Erc20`: + +```bash +# A Var in a submodule. +cargo run --bin odra_cli -- storage OwnedToken ownable.owner +# A Mapping entry. +cargo run --bin odra_cli -- storage OwnedToken erc20.balances --key account-hash-... +# A Mapping with a tuple key. +cargo run --bin odra_cli -- storage OwnedToken erc20.allowances --key account-hash-...:account-hash-... +# The number of items in a List, and the item at index 2. +cargo run --bin odra_cli -- storage OwnedToken holders.len +cargo run --bin odra_cli -- storage OwnedToken holders --key 2 +``` + +Modules that store data under Casper named keys or dictionaries (like the `Cep18` module) are +described the same way, and the command reads them from the right place. + +Add `--raw` to print the stored bytes hex-encoded instead of decoding them, and `--json` to get a +machine-readable report. `inspect` also includes the storage layout, so both are available to +external tooling. + ### Config command The `config` command prints the livenet configuration the CLI resolved, so you can confirm which network and account you are about to talk to before sending anything. From cab122f0c60917c36e6957a069cb265208bfe663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20P=C5=82askonka?= Date: Fri, 13 Nov 2026 12:18:08 +0100 Subject: [PATCH 2/2] Storage command and layout docs: match Odra 2.10, add to the 2.10 docs - Storage command: the output of the released CLI (paths to read, not-deployed value, JSON report), Cep18 examples that exist in the examples CLI. - Storage layout: the layout argument is odra::schema::named_key, list all storage macros. - The same sections in versioned_docs/version-2.10, where the feature shipped. Co-Authored-By: Claude Fable 5.2 --- docusaurus/docs/advanced/04-storage-layout.md | 5 +- docusaurus/docs/tutorials/odra-cli.md | 80 +++++++++++----- .../advanced/04-storage-layout.md | 45 +++++++++ .../version-2.10/tutorials/odra-cli.md | 96 +++++++++++++++++++ 4 files changed, 200 insertions(+), 26 deletions(-) diff --git a/docusaurus/docs/advanced/04-storage-layout.md b/docusaurus/docs/advanced/04-storage-layout.md index 5e1c461b5..12f33b9b3 100644 --- a/docusaurus/docs/advanced/04-storage-layout.md +++ b/docusaurus/docs/advanced/04-storage-layout.md @@ -142,10 +142,11 @@ example under named keys, declare their layout explicitly with the `layout` argument of the module attribute: ```rust -#[odra::module(layout = odra::schema::StorageKind::named_key::("decimals"))] +#[odra::module(layout = odra::schema::named_key::("decimals"))] pub struct Decimals; ``` The named-key storage macros (`single_value_storage!`, `key_value_storage!`, -`base64_encoded_key_value_storage!` and `compound_key_value_storage!`) do this +`base64_encoded_key_value_storage!`, `dictionary_value_storage!` and +`compound_key_value_storage!`) do this for you. diff --git a/docusaurus/docs/tutorials/odra-cli.md b/docusaurus/docs/tutorials/odra-cli.md index e70840168..8c35e5834 100644 --- a/docusaurus/docs/tutorials/odra-cli.md +++ b/docusaurus/docs/tutorials/odra-cli.md @@ -837,12 +837,12 @@ Running `inspect` without a contract name describes every registered contract. C ### Storage command -The `storage` command reads the state of a deployed contract straight from the storage, without -calling any entry point. It is handy when a value has no getter, or when you want to see exactly -what is stored and where. +The `storage` command reads the state of a contract straight from the storage, without calling any +entry point. It is handy when a value has no getter, or when you want to see exactly what is stored +and where. Run it with a contract name only to print the storage layout - every field with its index in the -module and the kind of storage behind it: +module and the kind of storage behind it, followed by the paths you can read: ```bash cargo run --bin odra_cli -- storage DogContract @@ -854,10 +854,16 @@ cargo run --bin odra_cli -- storage DogContract 💁 INFO : weight [2]: UINT 💁 INFO : name [3]: TEXT 💁 INFO : walks [4]: UINT (repeatable) +💁 INFO : Read with `storage DogContract [-k ]...`: +💁 INFO : barks: true|false +💁 INFO : weight: UINT +💁 INFO : name: TEXT +💁 INFO : walks: UINT (repeatable) ``` -Submodules are printed as nested trees, so the path of any field is the dotted chain of names -down the tree. Pass such a path to read the value: +The layout comes from the contract's code, so this works before anything is deployed. Submodules +are printed as nested trees, and the path of a field is the dotted chain of names down the tree +(`ownable.owner`). Pass a path to read the value: ```bash cargo run --bin odra_cli -- storage DogContract name @@ -867,37 +873,63 @@ cargo run --bin odra_cli -- storage DogContract name 💁 INFO : Contract: DogContract 💁 INFO : Path: name 💁 INFO : Type: TEXT -💁 INFO : Location: state[8a1f3c...] -💁 INFO : Value: "Mantus" +💁 INFO : Location: state[d236fca2b92ca42da90327820d7fe73c8ad22ea13cd8d761adc6e98822195c77] +💁 INFO : Value: ``` `Location` tells where the value lives: an item of the Odra `state` dictionary (with its key), a named key, or an item of a named dictionary. That is exactly what you would query with the node -RPC or a block explorer. +RPC or a block explorer, and it is printed even when the contract is not deployed, as above. For a +deployed contract `Value` is what is stored there, or `` if nothing is. Whenever the path goes through a `Mapping`, a `List` item or a dictionary, provide the key with `--key` (or `-k`), using the same value syntax as for entry point arguments. Repeat it for nested -containers, in path order. For an [OwnedToken](../examples/using-odra-modules) that composes -`Ownable` and `Erc20`: +containers, in path order. The layout lists the key each path needs. For the `Cep18` module, which +keeps its data under Casper named keys and dictionaries: ```bash -# A Var in a submodule. -cargo run --bin odra_cli -- storage OwnedToken ownable.owner -# A Mapping entry. -cargo run --bin odra_cli -- storage OwnedToken erc20.balances --key account-hash-... -# A Mapping with a tuple key. -cargo run --bin odra_cli -- storage OwnedToken erc20.allowances --key account-hash-...:account-hash-... -# The number of items in a List, and the item at index 2. -cargo run --bin odra_cli -- storage OwnedToken holders.len -cargo run --bin odra_cli -- storage OwnedToken holders --key 2 +💁 INFO : Read with `storage Cep18 [-k ]...`: +💁 INFO : decimals: 0-255|0x00|0b00000000 +💁 INFO : symbol: TEXT +💁 INFO : name: TEXT +💁 INFO : total_supply: DECIMAL +💁 INFO : balances -k : DECIMAL +💁 INFO : allowances -k : DECIMAL ``` -Modules that store data under Casper named keys or dictionaries (like the `Cep18` module) are -described the same way, and the command reads them from the right place. +```bash +# A value under a named key. +cargo run --bin odra_cli -- storage Cep18 total_supply +# A dictionary entry. +cargo run --bin odra_cli -- storage Cep18 balances -k account-hash-... +# An entry with a tuple key: the parts are joined with `:`. +cargo run --bin odra_cli -- storage Cep18 allowances -k account-hash-...:account-hash-... +``` + +For a `List` field, `.len` is the number of items and `-k ` reads one of them. + +[`inspect`](#inspect-command) prints the same storage layout next to the entry points, and the +[Storage Layout](../advanced/04-storage-layout.md#reading-the-storage-from-the-outside) article shows +how to resolve a path in your own code. Add `--raw` to print the stored bytes hex-encoded instead of decoding them, and `--json` to get a -machine-readable report. `inspect` also includes the storage layout, so both are available to -external tooling. +machine-readable report: + +```bash +cargo run --bin odra_cli -- storage DogContract name --json +``` + +```json +{ + "contract": "DogContract", + "path": "name", + "type": "TEXT", + "location": "state", + "key": "d236fca2b92ca42da90327820d7fe73c8ad22ea13cd8d761adc6e98822195c77", + "deployed": false, + "value": null +} +``` ### Config command diff --git a/docusaurus/versioned_docs/version-2.10/advanced/04-storage-layout.md b/docusaurus/versioned_docs/version-2.10/advanced/04-storage-layout.md index 24937c23a..12f33b9b3 100644 --- a/docusaurus/versioned_docs/version-2.10/advanced/04-storage-layout.md +++ b/docusaurus/versioned_docs/version-2.10/advanced/04-storage-layout.md @@ -105,3 +105,48 @@ encoded in hexadecimal format. Before being stored in the storage, each value is serialized into bytes using the `CLType` serialization method and subsequently encapsulated with Casper's `Bytes` types. + +## Reading the storage from the outside +You do not have to compute the keys by hand. Every module gets a generated +`odra::schema::SchemaStorageLayout` implementation that describes the layout +shown above as data: the index of each field, what is stored under it and the +types involved. `odra::schema::resolve_storage` walks that description for a +dotted field path (e.g. `borrowers.balances`) and the mapping keys, and returns +the storage location together with the type of the value: + +```rust +use odra::casper_types::bytesrepr::ToBytes; +use odra::schema::{resolve_storage, SchemaStorageLayout, StorageLocation}; + +let layout = Loans::storage_kind(); +let key = account.to_bytes().unwrap(); +let query = resolve_storage(&layout, "borrowers.balances", &[key]).unwrap(); + +if let StorageLocation::State { key } = &query.location { + // `key` is the `state` dictionary item key - read it with the node RPC, + // or directly through the host environment: + let bytes = env.get_storage_value(&contract_address, key.as_bytes()); +} +``` + +`HostEnv` exposes `get_storage_value`, `get_named_value` and +`get_dictionary_value` for the three kinds of locations, and they work on +every backend - OdraVM, CasperVM and livenet. + +The [Odra CLI](../tutorials/odra-cli#storage-command) wraps all of this in the +`storage` command, which prints the layout of a contract and reads any field +by its path. + +Modules that store data outside of the layout described in this article, for +example under named keys, declare their layout explicitly with the `layout` +argument of the module attribute: + +```rust +#[odra::module(layout = odra::schema::named_key::("decimals"))] +pub struct Decimals; +``` + +The named-key storage macros (`single_value_storage!`, `key_value_storage!`, +`base64_encoded_key_value_storage!`, `dictionary_value_storage!` and +`compound_key_value_storage!`) do this +for you. diff --git a/docusaurus/versioned_docs/version-2.10/tutorials/odra-cli.md b/docusaurus/versioned_docs/version-2.10/tutorials/odra-cli.md index adc9c5d95..8c35e5834 100644 --- a/docusaurus/versioned_docs/version-2.10/tutorials/odra-cli.md +++ b/docusaurus/versioned_docs/version-2.10/tutorials/odra-cli.md @@ -835,6 +835,102 @@ Types are shown as the CLI value hints you would type on the command line, not a Running `inspect` without a contract name describes every registered contract. Combined with `--json`, this is a convenient way to feed the contract interface to other tooling. +### Storage command + +The `storage` command reads the state of a contract straight from the storage, without calling any +entry point. It is handy when a value has no getter, or when you want to see exactly what is stored +and where. + +Run it with a contract name only to print the storage layout - every field with its index in the +module and the kind of storage behind it, followed by the paths you can read: + +```bash +cargo run --bin odra_cli -- storage DogContract +``` + +```bash +💁 INFO : Storage layout of DogContract (DogContract): +💁 INFO : barks [1]: true|false +💁 INFO : weight [2]: UINT +💁 INFO : name [3]: TEXT +💁 INFO : walks [4]: UINT (repeatable) +💁 INFO : Read with `storage DogContract [-k ]...`: +💁 INFO : barks: true|false +💁 INFO : weight: UINT +💁 INFO : name: TEXT +💁 INFO : walks: UINT (repeatable) +``` + +The layout comes from the contract's code, so this works before anything is deployed. Submodules +are printed as nested trees, and the path of a field is the dotted chain of names down the tree +(`ownable.owner`). Pass a path to read the value: + +```bash +cargo run --bin odra_cli -- storage DogContract name +``` + +```bash +💁 INFO : Contract: DogContract +💁 INFO : Path: name +💁 INFO : Type: TEXT +💁 INFO : Location: state[d236fca2b92ca42da90327820d7fe73c8ad22ea13cd8d761adc6e98822195c77] +💁 INFO : Value: +``` + +`Location` tells where the value lives: an item of the Odra `state` dictionary (with its key), a +named key, or an item of a named dictionary. That is exactly what you would query with the node +RPC or a block explorer, and it is printed even when the contract is not deployed, as above. For a +deployed contract `Value` is what is stored there, or `` if nothing is. + +Whenever the path goes through a `Mapping`, a `List` item or a dictionary, provide the key with +`--key` (or `-k`), using the same value syntax as for entry point arguments. Repeat it for nested +containers, in path order. The layout lists the key each path needs. For the `Cep18` module, which +keeps its data under Casper named keys and dictionaries: + +```bash +💁 INFO : Read with `storage Cep18 [-k ]...`: +💁 INFO : decimals: 0-255|0x00|0b00000000 +💁 INFO : symbol: TEXT +💁 INFO : name: TEXT +💁 INFO : total_supply: DECIMAL +💁 INFO : balances -k : DECIMAL +💁 INFO : allowances -k : DECIMAL +``` + +```bash +# A value under a named key. +cargo run --bin odra_cli -- storage Cep18 total_supply +# A dictionary entry. +cargo run --bin odra_cli -- storage Cep18 balances -k account-hash-... +# An entry with a tuple key: the parts are joined with `:`. +cargo run --bin odra_cli -- storage Cep18 allowances -k account-hash-...:account-hash-... +``` + +For a `List` field, `.len` is the number of items and `-k ` reads one of them. + +[`inspect`](#inspect-command) prints the same storage layout next to the entry points, and the +[Storage Layout](../advanced/04-storage-layout.md#reading-the-storage-from-the-outside) article shows +how to resolve a path in your own code. + +Add `--raw` to print the stored bytes hex-encoded instead of decoding them, and `--json` to get a +machine-readable report: + +```bash +cargo run --bin odra_cli -- storage DogContract name --json +``` + +```json +{ + "contract": "DogContract", + "path": "name", + "type": "TEXT", + "location": "state", + "key": "d236fca2b92ca42da90327820d7fe73c8ad22ea13cd8d761adc6e98822195c77", + "deployed": false, + "value": null +} +``` + ### Config command The `config` command prints the livenet configuration the CLI resolved, so you can confirm which network and account you are about to talk to before sending anything.