Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

Move Extension Basics
BeginnerChapter 1 of 415 min read

Module Structure

Every SUI Move smart contract starts with a module. Modules are the fundamental compilation unit -- they group related types, functions, and constants into a namespace that lives on-chain inside a published package.

Declaring a Module

SUI Move modules use the module keyword followed by package_name::module_name:

move
module tribe_storage_access::config;

This declares a module named config inside the tribe_storage_access package. The package name must match the name field in your Move.toml:

toml
[package]
name = "tribe_storage_access"
edition = "2024.beta"

[dependencies]
world = { local = "../../.world-contracts/contracts/world" }

Every .move file in the sources/ directory contains exactly one module.

Use Statements

Import types and functions from other modules with use:

move
use sui::dynamic_field as df;
use world::character::Character;
use world::storage_unit::{Self, StorageUnit};
  • use sui::dynamic_field as df -- imports the module and gives it a shorter alias.
  • use world::character::Character -- imports a single type.
  • use world::storage_unit::{Self, StorageUnit} -- Self imports the module itself (so you can call storage_unit::some_function()), and StorageUnit imports the type directly.

Struct Definitions and Abilities

Structs are how you define data in Move. Each struct can declare abilities that control what operations are allowed on instances of that type:

AbilityMeaning
keyCan be stored as a top-level object in global storage (requires an id: UID field)
storeCan be stored inside another object's fields
copyCan be duplicated with implicit copy
dropCan be silently discarded when it goes out of scope

Here is the ExtensionConfig struct from the tribe storage contract:

move
/// Shared configuration object. Rules are stored as dynamic fields.
public struct ExtensionConfig has key {
    id: UID,
}

This struct has key ability, which means it is a SUI object. The id: UID field is mandatory for any struct with key -- it gives the object a globally unique address on-chain.

The AdminCap adds store so it can be transferred and wrapped:

move
/// Capability proving ownership of this extension deployment.
public struct AdminCap has key, store {
    id: UID,
}

And the XAuth witness is a zero-sized struct with only drop:

move
/// Zero-sized typed witness for SSU extension authorization.
public struct XAuth has drop {}

We will cover the witness pattern in detail in the next chapter.

Function Visibility

SUI Move has three visibility levels for functions:

public -- Anyone Can Call

move
public fun has_rule<K: copy + drop + store>(config: &ExtensionConfig, key: K): bool {
    df::exists_(&config.id, key)
}

Any module in any package can call config::has_rule(...).

public(package) -- Same Package Only

move
/// Create an XAuth witness value. Only callable within this package.
public(package) fun x_auth(): XAuth {
    XAuth {}
}

Only modules inside tribe_storage_access can call config::x_auth(). External packages cannot. This is critical for security -- it means only your own modules can mint authorization witnesses.

Private (No Modifier) -- Same Module Only

move
fun init(ctx: &mut TxContext) {
    let admin_cap = AdminCap { id: object::new(ctx) };
    transfer::transfer(admin_cap, ctx.sender());

    let config = ExtensionConfig { id: object::new(ctx) };
    transfer::share_object(config);
}

The init function has no visibility modifier, making it private. In SUI Move, init is a special function called exactly once when the package is published. No one can call it again.

entry -- Transaction Entry Point

There is a fourth modifier, entry, for functions that should only be callable directly from a transaction (not from other Move code). It is useful for restricting composability when needed:

move
entry fun dangerous_admin_action(cap: &AdminCap) {
    // Only callable via a PTB, not from another module
}

Putting It All Together

Here is the complete config.move module that demonstrates all these patterns in a real EVE Frontier extension:

move
module tribe_storage_access::config;

use sui::dynamic_field as df;

// --- Structs with different abilities ---
public struct ExtensionConfig has key {
    id: UID,
}

public struct AdminCap has key, store {
    id: UID,
}

public struct XAuth has drop {}

// --- Package-private function ---
public(package) fun x_auth(): XAuth {
    XAuth {}
}

// --- Private init (runs once on publish) ---
fun init(ctx: &mut TxContext) {
    let admin_cap = AdminCap { id: object::new(ctx) };
    transfer::transfer(admin_cap, ctx.sender());

    let config = ExtensionConfig { id: object::new(ctx) };
    transfer::share_object(config);
}

// --- Public functions anyone can call ---
public fun has_rule<K: copy + drop + store>(
    config: &ExtensionConfig, key: K,
): bool {
    df::exists_(&config.id, key)
}

Key Takeaways

  • A module is declared with module package::name; and lives in a single .move file under sources/.
  • Structs with key are SUI objects and must have an id: UID field.
  • public functions are callable by anyone; public(package) restricts to the same package; private functions (no modifier) are module-only.
  • The init function runs once at publish time and is the place to create initial objects like config and capability tokens.
  • use statements import types and modules, with optional aliases via as.

Sign in to track your progress.