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 type | Storage parameter | Passed as |
|---|---|---|
Regular (no global) | user_storage: &UserStorage / &mut UserStorage | Each user’s personal shared object |
global: true | dapp_storage: &DappStorage / &mut DappStorage | The 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.