Using the generated Move code

After running dubhe generate, each resource in your config becomes a Move module under src/<name>/sources/codegen/resources/. This page shows how to use those modules in your system contracts.

Storage objects

Generated functions operate on storage objects rather than accepting a caller address explicitly:

Resource typeStorage parameterPassed as
Regular (no global)user_storage: &UserStorage / &mut UserStorageEach user’s personal shared object
global: truedapp_storage: &DappStorage / &mut DappStorageThe single DApp-wide shared object

A typical entry function signature looks like:

use dubhe::dapp_service::{DappStorage, UserStorage};
use dubhe::dapp_system;
use mygame::migrate;
use mygame::dapp_key::DappKey;
 
public entry fun my_action(
    dapp_storage: &DappStorage,       // guards + global resource reads
    user_storage: &mut UserStorage,   // per-user data reads and writes
    ctx: &mut TxContext,
) {
    // Version guard — block calls from stale package versions
    dapp_system::ensure_latest_version<DappKey>(dapp_storage, migrate::on_chain_version());
    // ... game logic using user_storage and dapp_storage ...
}

Checking existence

use mygame::level;
use mygame::stats;
use mygame::inventory;
use mygame::total_players;
 
public fun check_exists(
    dapp_storage: &DappStorage,
    user_storage: &UserStorage,
    item_id: u32,
) {
    // Per-user single-value resource
    let exists: bool = level::has(user_storage);
 
    // Per-user multi-field resource
    let exists: bool = stats::has(user_storage);
 
    // Keyed resource
    let exists: bool = inventory::has(user_storage, item_id);
 
    // Assert-style helpers (abort if condition not met)
    inventory::ensure_has(user_storage, item_id);     // abort if missing
    inventory::ensure_has_not(user_storage, item_id); // abort if already exists
 
    // Global resource — uses dapp_storage, not user_storage
    let exists: bool = total_players::has(dapp_storage);
}

Reading data

use mygame::level;
use mygame::stats;
use mygame::inventory;
use mygame::total_players;
 
public fun read_data(
    dapp_storage: &DappStorage,
    user_storage: &UserStorage,
    item_id: u32,
) {
    // Single value
    let lv: u32 = level::get(user_storage);
 
    // Multi-field — returns a tuple
    let (atk, hp, spd) = stats::get(user_storage);
 
    // Per-field accessor (no need to read the whole record)
    let atk: u32 = stats::get_attack(user_storage);
 
    // Struct accessor
    let s = stats::get_struct(user_storage);
    let atk: u32 = s.attack;
 
    // Keyed resource
    let qty: u32 = inventory::get(user_storage, item_id);
 
    // Global singleton — uses dapp_storage
    let count: u32 = total_players::get(dapp_storage);
}

Writing data

All write functions are public(package) — only modules in the same package can call them, enforcing access control.

use mygame::level;
use mygame::stats;
use mygame::inventory;
use mygame::total_players;
use mygame::facing;
use mygame::direction;
 
public entry fun level_up(
    dapp_storage: &mut DappStorage,   // mutable for global writes
    user_storage: &mut UserStorage,   // mutable for user writes
    ctx: &mut TxContext,
) {
    // Per-user single value
    let current = level::get(user_storage);
    level::set(user_storage, current + 1, ctx);
 
    // Multi-field (all fields at once)
    stats::set(user_storage, 150, 1000, 80, ctx); // attack, hp, defense
 
    // Per-field (partial update)
    stats::set_hp(user_storage, 500, ctx);
 
    // Keyed resource
    inventory::set(user_storage, 42u32, 10u32, ctx); // item_id=42, quantity=10
 
    // Global singleton
    let n = total_players::get(dapp_storage);
    total_players::set(dapp_storage, n + 1, ctx);
 
    // Enum value
    facing::set(user_storage, direction::new_north(), ctx);
}

Deleting data

use mygame::level;
use mygame::inventory;
 
public entry fun reset_player(
    user_storage: &mut UserStorage,
    ctx: &TxContext,
) {
    level::delete(user_storage, ctx);
    inventory::delete(user_storage, 42u32, ctx);
}

Offchain resources (events)

Offchain resources only have set. They emit storage events that indexers can pick up without writing to chain state.

use mygame::battle_log;
 
public entry fun finish_battle(
    user_storage: &mut UserStorage,
    enemy: address,
    won: bool,
    ctx: &mut TxContext,
) {
    // Emits an event — no on-chain storage written
    battle_log::set(user_storage, enemy, won, ctx);
}

Using custom errors

use mygame::error;
use mygame::level;
 
public entry fun attack(
    user_storage: &UserStorage,
    ctx: &mut TxContext,
) {
    // Pass `true` to continue, `false` to abort with the error
    error::player_not_found(level::has(user_storage));
 
    let lv = level::get(user_storage);
    // ... game logic
}

Complete system example

module mygame::combat_system;
 
use dubhe::dapp_service::{DappStorage, UserStorage};
use dubhe::dapp_system;
use mygame::dapp_key::DappKey;
use mygame::migrate;
use mygame::{level, stats, inventory, battle_log};
use mygame::error;
 
public entry fun attack_monster(
    dapp_storage: &DappStorage,
    user_storage: &mut UserStorage,
    monster: address,
    ctx: &mut TxContext,
) {
    // Guards
    dapp_system::ensure_latest_version<DappKey>(dapp_storage, migrate::on_chain_version());
    dapp_system::ensure_not_paused<DappKey>(dapp_storage);
 
    // Guard: player must be registered
    error::player_not_found(level::has(user_storage));
 
    // Read current state
    let lv = level::get(user_storage);
    let s  = stats::get_struct(user_storage);
 
    // Game logic
    let damage = s.attack * lv;
    let won = damage > 500;
 
    // Update state
    if (won) {
        level::set(user_storage, lv + 1, ctx);
        stats::set_attack(user_storage, s.attack + 10, ctx);
        inventory::set(user_storage, 1u32 /* reward item */, 1u32, ctx);
    };
 
    // Emit offchain event
    battle_log::set(user_storage, monster, won, ctx);
}

Deploy hook

The deploy hook runs automatically when genesis::run is called on first publish. Use it to initialize global singletons stored in DappStorage:

// src/<name>/sources/scripts/deploy_hook.move
module mygame::deploy_hook;
 
use dubhe::dapp_service::DappStorage;
use mygame::total_players;
 
public(package) fun run(dapp_storage: &mut DappStorage, ctx: &mut TxContext) {
    total_players::set(dapp_storage, 0, ctx);
}

Testing

init_test.move is generated automatically and provides helpers to create isolated storage objects in Move unit tests:

#[test_only]
module mygame::my_test;
 
use mygame::init_test;
use mygame::level;
 
#[test]
public fun test_level_up() {
    let ctx = &mut sui::tx_context::dummy();
 
    // Create isolated UserStorage for testing (no shared object overhead)
    let mut user_storage = init_test::create_user_storage_for_testing(@0xA, ctx);
 
    // Initial state: no level yet
    assert!(!level::has(&user_storage));
 
    // Set level
    level::set(&mut user_storage, 1, ctx);
    assert!(level::get(&user_storage) == 1);
 
    // Cleanup
    dubhe::dapp_service::destroy_user_storage(user_storage);
}

See Testing for full testing patterns including multi-caller scenarios and global resources.