Using the generated Move code
After running dubhe schemagen, 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.
Setup
All generated modules share the same two dependencies:
use dubhe::dapp_service::DappHub; // shared object passed to every entry function
use std::ascii::String; // used for resource_account (entity identifier)The resource_account parameter is a String representation of the entity’s address. In practice, call address_system::ensure_origin(ctx) to obtain it from the transaction sender:
use dubhe::address_system;
public entry fun my_action(dh: &mut DappHub, ctx: &mut TxContext) {
let player = address_system::ensure_origin(ctx); // returns String
// use `player` as resource_account in resource calls
}Checking existence
use example::level;
use example::stats;
use example::inventory;
public fun check_exists(dh: &DappHub, player: String, item_id: u32) {
// Entity single-value resource
let exists: bool = level::has(dh, player);
// Entity multi-field resource
let exists: bool = stats::has(dh, player);
// Keyed resource
let exists: bool = inventory::has(dh, player, item_id);
// Assert-style helpers (abort if condition not met)
inventory::ensure_has(dh, player, item_id); // abort if missing
inventory::ensure_has_not(dh, player, item_id); // abort if already exists
}For global resources, no resource_account is needed:
use example::total_players;
let exists: bool = total_players::has(dh);Reading data
use example::level;
use example::stats;
use example::inventory;
use example::total_players;
public fun read_data(dh: &DappHub, player: String, item_id: u32) {
// Single value
let lv: u32 = level::get(dh, player);
// Multi-field — returns a struct
let s = stats::get(dh, player);
let atk: u32 = s.attack;
let hp: u32 = s.hp;
// Per-field accessor (no need to read the whole struct)
let atk: u32 = stats::get_attack(dh, player);
// Keyed resource
let qty: u32 = inventory::get(dh, player, item_id);
// Global singleton
let count: u32 = total_players::get(dh);
}Writing data
All write functions are public(package) — only modules in the same package can call them, enforcing access control.
use example::level;
use example::stats;
use example::inventory;
use example::total_players;
use example::facing;
use example::direction;
public entry fun level_up(dh: &mut DappHub, ctx: &mut TxContext) {
let player = address_system::ensure_origin(ctx);
// Single value
let current = level::get(dh, player);
level::set(dh, player, current + 1, ctx);
// Multi-field (all fields at once)
stats::set(dh, player, 150, 1000, 80, ctx); // attack, hp, defense
// Per-field (partial update)
stats::set_hp(dh, player, 500, ctx);
// Keyed resource
inventory::set(dh, player, 42u32, 10u32, ctx); // item_id=42, quantity=10
// Global singleton
let n = total_players::get(dh);
total_players::set(dh, n + 1, ctx);
// Enum value
facing::set(dh, player, direction::new_north(), ctx);
}Deleting data
use example::level;
use example::inventory;
public entry fun reset_player(dh: &mut DappHub, ctx: &mut TxContext) {
let player = address_system::ensure_origin(ctx);
level::delete(dh, player);
inventory::delete(dh, player, 42u32);
}Offchain resources (events)
Offchain resources only have set. They emit storage events that indexers can pick up without writing to chain state.
use example::battle_log;
public entry fun finish_battle(
dh: &mut DappHub,
enemy: address,
won: bool,
ctx: &mut TxContext
) {
let player = address_system::ensure_origin(ctx);
// Emits an event — no on-chain storage written
battle_log::set(dh, player, enemy, won, ctx);
}Using custom errors
use example::errors::{player_not_found_error, game_over_error};
use example::level;
public entry fun attack(dh: &mut DappHub, ctx: &mut TxContext) {
let player = address_system::ensure_origin(ctx);
// Pass `true` to continue, `false` to abort with the error
player_not_found_error(level::has(dh, player));
let lv = level::get(dh, player);
// ... game logic
}Complete system example
module mygame::combat_system;
use dubhe::dapp_service::DappHub;
use dubhe::address_system;
use mygame::{level, stats, inventory, battle_log};
use mygame::errors::player_not_found_error;
public entry fun attack_monster(
dh: &mut DappHub,
monster: address,
ctx: &mut TxContext
) {
let player = address_system::ensure_origin(ctx);
// Guard: player must be registered
player_not_found_error(level::has(dh, player));
// Read current state
let lv = level::get(dh, player);
let s = stats::get(dh, player);
// Game logic
let damage = s.attack * lv;
let won = damage > 500;
// Update state
if (won) {
level::set(dh, player, lv + 1, ctx);
stats::set_attack(dh, player, s.attack + 10, ctx);
inventory::set(dh, player, 1u32 /* reward item */, 1u32, ctx);
};
// Emit offchain event
battle_log::set(dh, player, monster, won, ctx);
}Deploy hook
The deploy hook runs automatically when genesis::run is called on first publish. Use it to initialize global singletons:
// src/<name>/sources/scripts/deploy_hook.move
module mygame::deploy_hook;
use dubhe::dapp_service::DappHub;
use mygame::total_players;
public(package) fun run(dh: &mut DappHub, ctx: &mut TxContext) {
total_players::set(dh, 0, ctx);
}Testing
init_test.move is generated automatically and provides a helper to bootstrap the full dapp in Move unit tests:
#[test_only]
module mygame::my_test;
use sui::test_scenario;
use mygame::init_test;
use mygame::level;
use dubhe::address_system;
#[test]
public fun test_level_up() {
let player_addr = @0xA;
let mut scenario = test_scenario::begin(player_addr);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
let player = address_system::ensure_origin(ctx);
// Initial state: no level yet
assert!(!level::has(&dh, player));
// Set level
level::set(&mut dh, player, 1, ctx);
assert!(level::get(&dh, player) == 1);
dh.destroy();
scenario.end();
}