Schemas Config & schemagen
The schemagen 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: 'My awesome game',
resources: {}
});Run the generator:
dubhe schemagenThis 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
errors?: Record<string, string>; // Custom error messages
};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 package-level data that is shared by every user. No resource_account parameter is generated.
resources: {
total_supply: { global: true, fields: { value: 'u64' } },
config: { global: true, fields: { fee_rate: 'u32', paused: 'bool' } }
}Generated API:
total_supply::get(dh: &DappHub): u64
total_supply::set(dh: &mut DappHub, value: u64, ctx: &mut TxContext)
total_supply::has(dh: &DappHub): boolPattern 2 — Entity single value (shorthand)
Pass a MoveType string directly. The entity address string becomes the only lookup key.
resources: {
level: 'u32',
health: 'u64',
name: 'String'
}Generated API:
level::get(dh: &DappHub, resource_account: String): u32
level::set(dh: &mut DappHub, resource_account: String, value: u32, ctx: &mut TxContext)
level::has(dh: &DappHub, resource_account: String): boolPattern 3 — Entity multi-field record
Use fields without keys to store multiple values per entity. 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
stats::get(dh: &DappHub, resource_account: String): Stats
stats::set(dh: &mut DappHub, resource_account: String, attack: u32, hp: u32, speed: u32, ctx: &mut TxContext)
stats::has(dh: &DappHub, resource_account: String): bool
// Per-field
stats::get_attack(dh: &DappHub, resource_account: String): u32
stats::set_attack(dh: &mut DappHub, resource_account: String, attack: u32, ctx: &mut TxContext)
// … same for hp, speedPattern 4 — Keyed single value
Add keys to index by one or more extra dimensions beyond the entity address. If only one non-key field remains, a plain value (no struct) is generated.
resources: {
// player inventory: (entity, item_id) → quantity
inventory: {
fields: { item_id: 'u32', quantity: 'u32' },
keys: ['item_id']
}
}Generated API:
inventory::get(dh: &DappHub, resource_account: String, item_id: u32): u32
inventory::set(dh: &mut DappHub, resource_account: String, item_id: u32, quantity: u32, ctx: &mut TxContext)
inventory::has(dh: &DappHub, resource_account: String, item_id: u32): bool
inventory::ensure_has(dh: &DappHub, resource_account: String, item_id: u32)
inventory::ensure_has_not(dh: &DappHub, resource_account: String, item_id: u32)
inventory::delete(dh: &mut DappHub, resource_account: String, item_id: u32)Pattern 5 — Keyed multi-value record
Multiple keys and multiple remaining value fields. A struct is generated for the value side.
resources: {
// market orders: (entity, 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::get(dh: &DappHub, resource_account: String, seller: address, token_id: u32): Order
order::set(dh: &mut DappHub, resource_account: String, seller: address, token_id: u32, price: u64, amount: u32, ctx: &mut TxContext)
order::has(dh: &DappHub, resource_account: String, seller: address, token_id: u32): bool
order::delete(dh: &mut DappHub, resource_account: String, seller: address, token_id: u32)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(dh: &mut DappHub, resource_account: String, 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 example::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 example::errors::{not_enough_health_error, game_not_started_error};
// Passes condition (true = OK, false = abort)
not_enough_health_error(hero.hp > 0);
game_not_started_error(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-entity single value
level: 'u32',
// Per-entity multi-field record
stats: {
fields: { attack: 'u32', hp: 'u32', defense: 'u32' }
},
// Keyed by item_id — per-entity 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'
}
});