From 2abaa63ac129ece12fadbd13d99d762705681ead Mon Sep 17 00:00:00 2001 From: sklppy88 <152162806+sklppy88@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:48:04 +0000 Subject: [PATCH] chore(docs): add structs in map doc Co-authored-by: Esau --- .../smart_contracts/how_to_define_storage.md | 125 ++++++++- .../how_to_implement_custom_notes.md | 259 +++++++++++++----- 2 files changed, 315 insertions(+), 69 deletions(-) diff --git a/docs/docs/developers/docs/guides/smart_contracts/how_to_define_storage.md b/docs/docs/developers/docs/guides/smart_contracts/how_to_define_storage.md index 037e3510440a..cf93c0193a8d 100644 --- a/docs/docs/developers/docs/guides/smart_contracts/how_to_define_storage.md +++ b/docs/docs/developers/docs/guides/smart_contracts/how_to_define_storage.md @@ -92,7 +92,7 @@ Aztec.nr provides three private state variable types: - `PrivateImmutable`: Single immutable private value - `PrivateSet`: Collection of private notes -All private storage operates on note types rather than arbitrary data types. Learn how to implement custom notes [here](./how_to_implement_custom_notes.md) +All private storage operates on note types rather than arbitrary data types. Learn how to implement custom notes and use them with Maps [here](./how_to_implement_custom_notes.md) ### PrivateMutable @@ -388,6 +388,129 @@ unconstrained fn get_public_immutable() -> MyStruct { } ``` +## Use custom structs in public storage + +Both `PublicMutable` and `PublicImmutable` are generic over any serializable type, which means you can store custom structs in public storage. This is useful for storing configuration data, game state, or any other structured data that needs to be publicly visible. + +### Define a custom struct for storage + +To use a custom struct in public storage, it must implement the `Packable` trait. You can automatically derive this along with other useful traits: + +```rust +use dep::aztec::protocol_types::{ + address::AztecAddress, + traits::{Deserialize, Packable, Serialize} +}; + +// Required derives for public storage: +// - Packable: Required for all public storage +// - Serialize: Required for returning from functions +// - Deserialize: Required for receiving as parameters +#[derive(Deserialize, Packable, Serialize)] +pub struct Asset { + pub interest_accumulator: u128, + pub last_updated_ts: u64, + pub loan_to_value: u128, + pub oracle: AztecAddress, +} +``` + +Common optional derives include: +- `Eq`: For equality comparisons between structs + +### Store custom structs + +Once defined, use your custom struct in storage declarations: + +```rust +#[storage] +struct Storage { + // Single custom struct + config: PublicMutable, + + // Map of custom structs + assets: Map, Context>, + + // Immutable custom struct (like contract config) + initial_config: PublicImmutable, +} +``` + +### Read and write custom structs + +Work with custom structs using the same `read()` and `write()` methods as built-in types: + +```rust +#[public] +fn update_asset(asset_id: Field, new_accumulator: u128) { + // Read the current struct + let mut asset = storage.assets.at(asset_id).read(); + + // Modify fields + asset.interest_accumulator = new_accumulator; + asset.last_updated_ts = context.timestamp(); + + // Write back the updated struct + storage.assets.at(asset_id).write(asset); +} + +#[public] +fn get_asset(asset_id: Field) -> Asset { + storage.assets.at(asset_id).read() +} +``` + +You can also create and store new struct instances: + +```rust +#[public] +fn initialize_asset( + interest_accumulator: u128, + loan_to_value: u128, + oracle: AztecAddress +) { + let last_updated_ts = context.timestamp() as u64; + + storage.assets.at(0).write( + Asset { + interest_accumulator, + last_updated_ts, + loan_to_value, + oracle, + } + ); +} +``` + +### Use custom structs in nested maps + +Custom structs work seamlessly with nested map structures: + +```rust +#[derive(Deserialize, Eq, Packable, Serialize)] +pub struct Game { + pub started: bool, + pub finished: bool, + pub current_round: u32, +} + +#[storage] +struct Storage { + // Map game_id -> player_address -> Game struct + games: Map, Context>, Context>, +} + +#[public] +fn start_game(game_id: Field, player: AztecAddress) { + let game = Game { + started: true, + finished: false, + current_round: 0, + }; + storage.games.at(game_id).at(player).write(game); +} +``` + ## Delayed Public Mutable This storage type is used if you want to use public values in private execution. diff --git a/docs/docs/developers/docs/guides/smart_contracts/how_to_implement_custom_notes.md b/docs/docs/developers/docs/guides/smart_contracts/how_to_implement_custom_notes.md index eb24d09c8109..dff6ca8679f1 100644 --- a/docs/docs/developers/docs/guides/smart_contracts/how_to_implement_custom_notes.md +++ b/docs/docs/developers/docs/guides/smart_contracts/how_to_implement_custom_notes.md @@ -198,138 +198,261 @@ impl NoteHash for TransparentNote { This pattern is useful for "shielding" tokens - creating notes in public that can be redeemed in private by anyone who knows the secret. -## Using custom notes in storage +## Basic usage in storage -Declare your custom note type in contract storage: +Before diving into Maps, let's understand basic custom note usage. + +### Declare storage ```rust +use dep::aztec::state_vars::{PrivateSet, PrivateImmutable}; + #[storage] struct Storage { - // Map from owner address to their notes - private_notes: Map, Context>, + // Collection of notes for a single owner + balances: PrivateSet, - // Single immutable note - config_note: PrivateImmutable, + // Single immutable configuration + config: PrivateImmutable, } ``` -## Working with custom notes - -### Creating and storing notes +### Insert notes ```rust +use dep::aztec::messages::message_delivery::MessageDelivery; + #[external("private")] -fn create_note(owner: AztecAddress, value: Field, data: u32) { - // Create the note +fn create_note(value: Field, data: u32) { + let owner = context.msg_sender(); let note = CustomNote::new(value, data, owner); - // Store it in the owner's note set - storage.private_notes.at(owner).insert(note); + storage.balances + .insert(note) + .emit(&mut context, owner, MessageDelivery.CONSTRAINED_ONCHAIN); } ``` -### Reading notes +### Read notes ```rust -use aztec::note::note_getter_options::NoteGetterOptions; +use dep::aztec::note::note_getter_options::NoteGetterOptions; #[external("private")] -fn get_notes(owner: AztecAddress) -> BoundedVec { - // Get all notes for the owner - let notes = storage.private_notes.at(owner).get_notes( - NoteGetterOptions::new() - ); - - notes +fn get_notes() -> BoundedVec { + storage.balances.get_notes(NoteGetterOptions::new()) } #[external("private")] -fn find_note_by_value(owner: AztecAddress, target_value: Field) -> CustomNote { +fn find_note_by_value(target_value: Field) -> CustomNote { let options = NoteGetterOptions::new() .select(CustomNote::properties().value, target_value, Option::none()) .set_limit(1); - let notes = storage.private_notes.at(owner).get_notes(options); + let notes = storage.balances.get_notes(options); assert(notes.len() == 1, "Note not found"); - notes.get(0) } ``` -### Transferring notes - -To transfer a custom note between users: +### Transfer notes ```rust #[external("private")] -fn transfer_note(from: AztecAddress, to: AztecAddress, value: Field) { - // Find and remove from sender (nullifies the old note) - let note = find_note_by_value(from, value); - storage.private_notes.at(from).remove(note); +fn transfer_note(to: AztecAddress, value: Field) { + // Find and remove from sender + let note = find_note_by_value(value); + storage.balances.remove(note); - // Create new note for recipient with same value but new owner + // Create new note for recipient let new_note = CustomNote::new(note.value, note.data, to); - storage.private_notes.at(to).insert(new_note); + storage.balances.insert(new_note) + .emit(&mut context, to, MessageDelivery.CONSTRAINED_ONCHAIN); } ``` -## Common patterns +## Using custom notes with Maps -### Singleton notes +Maps are essential for organizing custom notes by key in private storage. They allow you to efficiently store and retrieve notes based on addresses, IDs, or other identifiers. -For data that should have only one instance per user: +### Common Map patterns ```rust +use dep::aztec::{ + macros::notes::note, + oracle::random::random, + protocol_types::{address::AztecAddress, traits::Packable}, + state_vars::{Map, PrivateMutable, PrivateSet}, +}; + +#[derive(Eq, Packable)] #[note] -pub struct ProfileNote { +pub struct CardNote { + points: u32, + strength: u32, owner: AztecAddress, - data: Field, randomness: Field, } +impl CardNote { + pub fn new(points: u32, strength: u32, owner: AztecAddress) -> Self { + let randomness = unsafe { random() }; + CardNote { points, strength, owner, randomness } + } +} + +#[storage] +struct Storage { + // Map from player address to their collection of cards + card_collections: Map, Context>, + + // Map from player address to their active card + active_cards: Map, Context>, + + // Nested maps: game_id -> player -> cards + game_cards: Map, Context>, Context>, +} +``` + +Common patterns: +- `Map>` - Multiple notes per user (like token balances, card collections) +- `Map>` - Single note per user (like user profile, active state) +- `Map>>` - Nested organization (game sessions, channels) + +### Inserting into mapped PrivateSets + +To add notes to a mapped PrivateSet: + +```rust +use dep::aztec::messages::message_delivery::MessageDelivery; + #[external("private")] -fn update_profile(new_data: Field) { - let owner = context.msg_sender(); +fn add_card_to_collection(player: AztecAddress, points: u32, strength: u32) { + let card = CardNote::new(points, strength, player); + + // Insert into the player's collection + storage.card_collections + .at(player) + .insert(card) + .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); +} +``` + +### Using mapped PrivateMutable - // Remove old profile if exists - let old_notes = storage.profiles.at(owner).get_notes( - NoteGetterOptions::new().set_limit(1) - ); - if old_notes.len() > 0 { - storage.profiles.at(owner).remove(old_notes[0]); +For PrivateMutable in a Map, handle both initialization and updates: + +```rust +use dep::aztec::messages::message_delivery::MessageDelivery; + +#[external("private")] +fn set_active_card(player: AztecAddress, points: u32, strength: u32) { + // Check if already initialized + let is_initialized = storage.active_cards.at(player).is_initialized(); + + if is_initialized { + // Replace existing card + storage.active_cards + .at(player) + .replace(|_old_card| CardNote::new(points, strength, player)) + .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); + } else { + // Initialize for first time + let card = CardNote::new(points, strength, player); + storage.active_cards + .at(player) + .initialize(card) + .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); } +} +``` + +### Reading from mapped PrivateSets - // Create new profile - let new_profile = ProfileNote::new(owner, new_data); - storage.profiles.at(owner).insert(new_profile); +```rust +use dep::aztec::note::note_getter_options::NoteGetterOptions; + +#[external("private")] +fn get_player_cards(player: AztecAddress) -> BoundedVec { + // Get all cards for this player + storage.card_collections + .at(player) + .get_notes(NoteGetterOptions::new()) +} + +#[external("private")] +fn get_total_points(player: AztecAddress) -> u32 { + let options = NoteGetterOptions::new(); + let notes = storage.card_collections.at(player).get_notes(options); + + let mut total = 0; + for i in 0..notes.len() { + let card = notes.get(i); + total += card.points; + } + total } ``` -### Filtering notes +### Reading from mapped PrivateMutable -For efficient lookups by specific fields: +```rust +#[external("private")] +fn get_active_card(player: AztecAddress) -> CardNote { + storage.active_cards.at(player).get_note() +} +``` + +### Filtering notes in Maps + +Filter notes by their fields when reading from maps: ```rust -use aztec::note::note_getter_options::{NoteGetterOptions, PropertySelector}; +use dep::aztec::{note::note_getter_options::NoteGetterOptions, utils::comparison::Comparator}; -#[derive(Eq, Packable)] -#[note] -pub struct OrderNote { - order_id: Field, // Field we want to filter by - amount: u128, - owner: AztecAddress, - randomness: Field, +#[external("private")] +fn find_strong_cards(player: AztecAddress, min_strength: u32) -> BoundedVec { + let options = NoteGetterOptions::new() + .select(CardNote::properties().strength, Comparator.GTE, min_strength) + .set_limit(10); + + storage.card_collections.at(player).get_notes(options) } +``` -// Usage - filter by order_id -fn get_order(owner: AztecAddress, target_id: Field) -> OrderNote { - let options = NoteGetterOptions::new() - .select(OrderNote::properties().order_id, target_id, Option::none()) - .set_limit(1); +### Working with nested Maps - let notes = storage.orders.at(owner).get_notes(options); - assert(notes.len() == 1, "Order not found"); - notes.get(0) +Navigate nested map structures to organize data hierarchically: + +```rust +use dep::aztec::messages::message_delivery::MessageDelivery; + +#[external("private")] +fn add_card_to_game( + game_id: Field, + player: AztecAddress, + points: u32, + strength: u32 +) { + let card = CardNote::new(points, strength, player); + + // Navigate nested maps: game_cards[game_id][player] + storage.game_cards + .at(game_id) + .at(player) + .insert(card) + .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); +} + +#[external("private")] +fn get_game_cards( + game_id: Field, + player: AztecAddress +) -> BoundedVec { + storage.game_cards + .at(game_id) + .at(player) + .get_notes(NoteGetterOptions::new()) } ```