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 schemagen

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
  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): bool

Pattern 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): bool

Pattern 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, speed

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