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 generate

This 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 typeWhen usedPassed as
UserStorageDefault — per-user dataA shared object owned by each individual user
DappStorageglobal: true — DApp-wide dataA single shared object per DApp
  • UserStorage — Created once per user (via dapp_system::create_user_storage). Write functions take user_storage: &mut UserStorage. Read functions take user_storage: &UserStorage.
  • DappStorage — One per DApp, created during genesis::run. Write functions take dapp_storage: &mut DappStorage. Read functions take dapp_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 DappStorage per DApp. Transactions that write to a global resource (&mut DappStorage) are serialized. Reserve global: true for 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, speed

Pattern 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'
  }
});