Data Structures and Storage

Define Your Own Data Structures

Both Solana and Aptos let developers define custom data structures easily. Take the todo list app as an example.

On Solana, we define the data structure as:


use anchor_lang::prelude::*;
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)]
pub struct Todo {
#[max_len(200)] // Reserve space for up to 50 characters
pub content: String,
pub completed: bool,
}
// Each todo list is stored in its own account
#[account]
#[derive(InitSpace)]
pub struct TodoList {
pub owner: Pubkey,
#[max_len(10)] // Reserve space for up to 10 todos
pub todos: Vec<Todo>,
}

On Aptos, we define the data structure as:


struct Todo has store, drop, copy {
content: String,
completed: bool,
}
// Each todo list is stored as a resource
struct TodoList has key {
owner: address,
todos: vector<Todo>,
}


Where to Store the Data

On Solana, TodoList is stored under an account owned by the todo list program. But on Aptos, there are several options to store the TodoList resource.

  • Under the todo list owner: This is the simplest approach and makes it easy to retrieve the TodoList resource. But it's not recommended for production apps, because each address can have at most one resource of a certain type (e.g. TodoList). If we place the TodoList resource under the owner's address, the owner can only have one todo list.

// error codes
/// Try to create another todo list, but each user can only have one todo list
const E_EACH_USER_CAN_ONLY_HAVE_ONE_TODO_LIST: u64 = 1;
public entry fun create_todo_list(sender: &signer) {
let sender_address = signer::address_of(sender);
assert!(
!exists<TodoList>(sender_address),
E_EACH_USER_CAN_ONLY_HAVE_ONE_TODO_LIST
);
let todo_list = TodoList {
owner: sender_address,
todos: vector::empty(),
};
// store the TodoList resource directly under the sender
move_to(sender, todo_list);
}
#[view]
public fun get_todo_list(sender: address): (address, u64) acquires TodoList {
// borrow the TodoList resource from the sender
let todo_list = borrow_global<TodoList>(sender);
(todo_list.owner, vector::length(&todo_list.todos))
}

  • Under an Object: This is the most scalable approach, by storing the TodoList resource under an object, each user can have multiple todo lists by owning multiple objects. We can use create_named_object to deterministically generate the address of an object based on the user address and a seed (e.g. how many todo lists the user has). Named object address is similar to PDA on Solana.

use std::bcs;
use std::signer;
use aptos_framework::object;
use aptos_framework::object::Object;
struct UserTodoListCounter has key {
counter: u64,
}
public entry fun create_todo_list(sender: &signer) acquires UserTodoListCounter {
let sender_address = signer::address_of(sender);
let counter = if (exists<UserTodoListCounter>(sender_address)) {
let counter = borrow_global<UserTodoListCounter>(sender_address);
counter.counter
} else {
let counter = UserTodoListCounter { counter: 0 };
// store the UserTodoListCounter resource directly under the sender
move_to(sender, counter);
0
};
// create a new object to hold the todo list, use the counter as seed
let obj_holds_todo_list = object::create_named_object(sender, bcs::to_bytes(&counter));
let obj_signer = object::generate_signer(&obj_holds_todo_list);
let todo_list = TodoList {
owner: sender_address,
todos: vector::empty(),
};
// store the TodoList resource under the newly created object
move_to(&obj_signer, todo_list);
// increment the counter
let counter = borrow_global_mut<UserTodoListCounter>(sender_address);
counter.counter = counter.counter + 1;
}
#[view]
public fun get_todo_list(sender: address, todo_list_idx: u64): (address, u64) acquires TodoList {
let todo_list_obj_addr = object::create_object_address(&sender, bcs::to_bytes(&todo_list_idx));
let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
(todo_list.owner, vector::length(&todo_list.todos))
}

  • Under a Resource Account: Resource Account is what we used before object, we recommend using Object instead of Resource Account now.

Bonus: Aptos Supports Map Data Structure

Unlike Solana where developers always need to use multiple accounts to simulate a map data structure, Aptos shipped with built-in Map called table at 0x1::table. In the todo list app, we can replace the TodoList with a new struct AllTodoLists:


use aptos_std::table::Table;
struct AllTodoLists has key {
todo_lists: Table<address, vector<Todo>>,
}

This way we only need 1 AllTodoLists resource to store all todo lists. We can store it under the todo list app address, for easy access. However, if the app grows, it's better to use the TodoList struct and store each todo list under an object for better scalability.