Example Program Using Structs
Let's build a practical example that uses structs to create a simple on-chain address book. This will demonstrate struct definition, creation, storage, reading, updating, and destruction.
The Address Book Contract
/// A simple on-chain address book that stores contact information.
module my_addr::address_book {
use std::signer;
use std::string::String;
use std::vector;
/// No address book found for this account
const E_NO_ADDRESS_BOOK: u64 = 1;
/// Contact not found in the address book
const E_CONTACT_NOT_FOUND: u64 = 2;
/// Address book already exists
const E_ALREADY_EXISTS: u64 = 3;
/// A single contact entry
struct Contact has store, drop, copy {
name: String,
phone: String,
}
/// The address book resource stored under an account
struct AddressBook has key {
contacts: vector<Contact>,
owner: address,
}
/// Create a new address book for the caller
public entry fun create_address_book(account: &signer) {
let addr = signer::address_of(account);
assert!(!exists<AddressBook>(addr), E_ALREADY_EXISTS);
move_to(account, AddressBook {
contacts: vector[],
owner: addr,
});
}
/// Add a contact to the address book
public entry fun add_contact(
account: &signer,
name: String,
phone: String,
) acquires AddressBook {
let addr = signer::address_of(account);
assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
let book = &mut AddressBook[addr];
book.contacts.push_back(Contact { name, phone });
}
/// Remove a contact by index
public entry fun remove_contact(
account: &signer,
index: u64,
) acquires AddressBook {
let addr = signer::address_of(account);
assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
let book = &mut AddressBook[addr];
assert!(index < book.contacts.length(), E_CONTACT_NOT_FOUND);
book.contacts.remove(index);
}
/// View all contacts
#[view]
public fun get_contacts(addr: address): vector<Contact> acquires AddressBook {
assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
AddressBook[addr].contacts
}
/// View the number of contacts
#[view]
public fun contact_count(addr: address): u64 acquires AddressBook {
assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
AddressBook[addr].contacts.length()
}
/// Remove the address book entirely
public entry fun delete_address_book(account: &signer) acquires AddressBook {
let addr = signer::address_of(account);
assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
let AddressBook { contacts: _, owner: _ } = move_from<AddressBook>(addr);
}
// ---- Tests ----
#[test(account = @0x1)]
fun test_create_and_add_contact(account: &signer) acquires AddressBook {
let addr = signer::address_of(account);
create_address_book(account);
assert!(exists<AddressBook>(addr));
assert!(contact_count(addr) == 0);
add_contact(
account,
std::string::utf8(b"Alice"),
std::string::utf8(b"555-1234"),
);
assert!(contact_count(addr) == 1);
let contacts = get_contacts(addr);
let first = contacts[0];
assert!(first.name == std::string::utf8(b"Alice"));
}
#[test(account = @0x1)]
fun test_delete_address_book(account: &signer) acquires AddressBook {
let addr = signer::address_of(account);
create_address_book(account);
assert!(exists<AddressBook>(addr));
delete_address_book(account);
assert!(!exists<AddressBook>(addr));
}
#[test(account = @0x1)]
fun test_remove_contact(account: &signer) acquires AddressBook {
let addr = signer::address_of(account);
create_address_book(account);
add_contact(account, std::string::utf8(b"Alice"), std::string::utf8(b"555-1234"));
add_contact(account, std::string::utf8(b"Bob"), std::string::utf8(b"555-5678"));
assert!(contact_count(addr) == 2);
remove_contact(account, 0);
assert!(contact_count(addr) == 1);
}
#[test(account = @0x1)]
#[expected_failure(abort_code = E_ALREADY_EXISTS)]
fun test_double_create_fails(account: &signer) {
create_address_book(account);
create_address_book(account);
}
#[test(account = @0x1)]
#[expected_failure(abort_code = E_CONTACT_NOT_FOUND)]
fun test_remove_invalid_index_fails(account: &signer) acquires AddressBook {
create_address_book(account);
remove_contact(account, 0); // No contacts -- should fail
}
}
Breakdown
Struct Design
Contacthasstore,drop, andcopy-- it is a lightweight data value that can be freely duplicated and is embeddable inside other stored types.AddressBookhas onlykey-- it is a resource that lives in global storage and cannot be copied or accidentally dropped.
Entry Functions
create_address_bookstores a new empty address book under the caller's account.add_contactappends a contact to the caller's existing address book.remove_contactremoves a contact by index, usingE_CONTACT_NOT_FOUNDif the index is out of bounds.delete_address_bookremoves and destructures the resource, freeing the storage slot.
View Functions
get_contactsreturns a copy of all contacts.contact_countreturns the number of contacts.
Error Handling
Each function validates preconditions with assert! and descriptive error codes. This is a best practice for all Move contracts.
Tests
The test functions verify the full lifecycle: creation, modification, reading, and deletion. The #[expected_failure] test ensures that duplicate creation is properly rejected.
Key Takeaways
- Structs model your domain: Use them to represent entities and their relationships.
- Abilities control behavior: Choose abilities carefully to enforce safety properties.
- Resources live in global storage: Use
move_to,move_from, and borrowing operations. - Test everything: Move's built-in testing makes it easy to verify correctness.