Resources config & generate
The generate CLI tool reads a dubhe.config.ts file and generates typed Move modules for each resource. It eliminates the need to write storage boilerplate by hand and ensures all on-chain reads/writes are type-safe.
Minimal config
Place a dubhe.config.ts at the root of your contracts directory:
import { defineConfig } from '@0xobelisk/sui-common';
export const dubheConfig = defineConfig({
name: 'my_game',
description: 'Example on-chain game',
resources: {}
});Run the generator:
dubhe generateThis creates a src/<name>/sources/codegen/ directory with all generated Move modules.
Config Reference
type DubheConfig = {
name: string; // Move package name
description: string; // Dapp description
enums?: Record<string, string[]>; // Custom enum types
resources?: Record<string, Component | MoveType>; // All data definitions (optional)
errors?: Record<string, string | { message: string }>; // Custom error messages
};Storage Objects
Dubhe generates two kinds of storage objects. Which one a resource uses depends on the global flag:
| Storage type | When used | Passed as |
|---|---|---|
UserStorage | Default — per-user data | A shared object owned by each individual user |
DappStorage | global: true — DApp-wide data | A single shared object per DApp |
UserStorage— Created once per user (viadapp_system::create_user_storage). Write functions takeuser_storage: &mut UserStorage. Read functions takeuser_storage: &UserStorage.DappStorage— One per DApp, created duringgenesis::run. Write functions takedapp_storage: &mut DappStorage. Read functions takedapp_storage: &DappStorage.
Entry functions typically receive both:
public entry fun my_action(
dapp_storage: &DappStorage, // read-only: guards, global reads
user_storage: &mut UserStorage, // read-write: user data
ctx: &mut TxContext,
) { ... }Resource Patterns
All data is defined under resources. There are six patterns depending on the combination of global, keys, and fields.
Pattern 1 — Global singleton
Use global: true for DApp-wide data shared by all users. Generated functions take dapp_storage instead of user_storage.
Performance note: There is only one
DappStorageper DApp. Transactions that write to a global resource (&mut DappStorage) are serialized. Reserveglobal: truefor infrequently written data such as config flags or aggregate counters. Avoid it for state that every user transaction updates.
resources: {
total_supply: { global: true, fields: { value: 'u64' } },
config: { global: true, fields: { fee_rate: 'u32', paused: 'bool' } }
}Generated API:
// Read (public)
total_supply::has(dapp_storage: &DappStorage): bool
total_supply::get(dapp_storage: &DappStorage): u64
// Write (public(package) — only your own package can call)
total_supply::set(dapp_storage: &mut DappStorage, value: u64, ctx: &mut TxContext)
total_supply::delete(dapp_storage: &mut DappStorage)Pattern 2 — User single value (shorthand)
Pass a MoveType string directly. Data is stored per UserStorage instance — one record per user.
resources: {
level: 'u32',
health: 'u64',
name: 'String'
}Generated API:
// Read (public)
level::has(user_storage: &UserStorage): bool
level::get(user_storage: &UserStorage): u32
// Write (public(package))
level::set(user_storage: &mut UserStorage, value: u32, ctx: &mut TxContext)
level::delete(user_storage: &mut UserStorage, ctx: &TxContext)Pattern 3 — User multi-field record
Use fields without keys to store multiple values per user. A Move struct is generated for bulk access; individual field accessors are also generated.
resources: {
stats: {
fields: { attack: 'u32', hp: 'u32', speed: 'u32' }
}
}Generated API:
// Struct
public struct Stats has copy, drop, store { attack: u32, hp: u32, speed: u32 }
// Bulk read/write
stats::has(user_storage: &UserStorage): bool
stats::get(user_storage: &UserStorage): (u32, u32, u32) // (attack, hp, speed)
stats::set(user_storage: &mut UserStorage, attack: u32, hp: u32, speed: u32, ctx: &mut TxContext)
stats::get_struct(user_storage: &UserStorage): Stats
stats::set_struct(user_storage: &mut UserStorage, stats: Stats, ctx: &mut TxContext)
// Per-field accessors
stats::get_attack(user_storage: &UserStorage): u32
stats::set_attack(user_storage: &mut UserStorage, attack: u32, ctx: &mut TxContext)
// … same for hp, speedPattern 4 — Keyed single value
Add keys to index by one or more extra dimensions. If only one non-key field remains, a plain value (no struct) is generated.
resources: {
// per-user inventory: item_id → quantity
inventory: {
fields: { item_id: 'u32', quantity: 'u32' },
keys: ['item_id']
}
}Generated API:
inventory::has(user_storage: &UserStorage, item_id: u32): bool
inventory::get(user_storage: &UserStorage, item_id: u32): u32
inventory::set(user_storage: &mut UserStorage, item_id: u32, quantity: u32, ctx: &mut TxContext)
inventory::ensure_has(user_storage: &UserStorage, item_id: u32)
inventory::ensure_has_not(user_storage: &UserStorage, item_id: u32)
inventory::delete(user_storage: &mut UserStorage, item_id: u32, ctx: &TxContext)Pattern 5 — Keyed multi-value record
Multiple keys and multiple remaining value fields. A struct is generated for the value side.
resources: {
// per-user market orders: (seller, token_id) → { price, amount }
order: {
fields: { seller: 'address', token_id: 'u32', price: 'u64', amount: 'u32' },
keys: ['seller', 'token_id']
}
}Generated API:
public struct Order has copy, drop, store { price: u64, amount: u32 }
order::has(user_storage: &UserStorage, seller: address, token_id: u32): bool
order::get(user_storage: &UserStorage, seller: address, token_id: u32): (u64, u32)
order::set(user_storage: &mut UserStorage, seller: address, token_id: u32, price: u64, amount: u32, ctx: &mut TxContext)
order::get_struct(user_storage: &UserStorage, seller: address, token_id: u32): Order
order::set_struct(user_storage: &mut UserStorage, seller: address, token_id: u32, order: Order, ctx: &mut TxContext)
order::delete(user_storage: &mut UserStorage, seller: address, token_id: u32, ctx: &TxContext)Pattern 6 — Offchain (event-only)
Set offchain: true to emit storage events without writing to chain state. Only set is generated (no get / has).
resources: {
battle_result: {
offchain: true,
fields: { monster: 'address', result: 'bool', damage: 'u32' },
keys: ['monster']
}
}Generated API:
// Only set — data is indexed off-chain via events
battle_result::set(user_storage: &mut UserStorage, monster: address, result: bool, damage: u32, ctx: &mut TxContext)Enums
Define shared enum types that can be used as field or key types across any resource.
enums: {
Direction: ['North', 'East', 'South', 'West'],
Status: ['Idle', 'Fighting', 'Dead']
}Each enum generates a module with constructors, matchers, and BCS encode/decode helpers:
use mygame::direction::{Self, Direction};
let dir = direction::new_north();
direction::is_east(&dir); // false
// Use as a field or key in a resource
resources: {
facing: 'Direction',
quest: { fields: { target: 'address', status: 'Status' }, keys: ['target'] }
}Errors
Custom abort codes with readable messages:
errors: {
not_enough_health: "HP is too low to perform this action",
game_not_started: "The game has not started yet"
}Generated:
use mygame::error;
// Passes success condition (true = OK, false = abort)
error::not_enough_health(hero.hp > 0);
error::game_not_started(game.started);Full example
import { defineConfig } from '@0xobelisk/sui-common';
export const dubheConfig = defineConfig({
name: 'mygame',
description: 'A simple on-chain game',
enums: {
Direction: ['North', 'East', 'South', 'West']
},
resources: {
// Global singleton — shared across all players
total_players: { global: true, fields: { count: 'u32' } },
// Per-user single value
level: 'u32',
// Per-user multi-field record
stats: {
fields: { attack: 'u32', hp: 'u32', defense: 'u32' }
},
// Keyed by item_id — per-user inventory
inventory: {
fields: { item_id: 'u32', quantity: 'u32' },
keys: ['item_id']
},
// Enum as value type
facing: 'Direction',
// Offchain event — game result notification
battle_log: {
offchain: true,
fields: { enemy: 'address', won: 'bool' },
keys: ['enemy']
}
},
errors: {
player_not_found: 'Player does not exist',
game_over: 'The game has already ended'
}
});