Resources

What are Resources?

In Dubhe, resources are the typed on-chain storage layer. When building with Dubhe, developers declare all contract state under the resources key in dubhe.config.ts. The generate CLI then produces Move modules that handle storage reads, writes, and events automatically.

This replaces the traditional Move approach of defining data structures and table access manually:

Traditional Move storage

// Declare manually
struct Player has store {
    level: u32,
    hp: u64,
}
 
// Store and retrieve manually
let player = Player { level: 1, hp: 100 };
table::add(&mut storage, addr, player);
let p = table::borrow(&storage, addr);

Dubhe resources (dubhe.config.ts)

// dubhe.config.ts — declare once
resources: {
  level: 'u32',
  hp:    'u64',
}
// Generated Move — type-safe, no boilerplate
level::set(&mut user_storage, 1, ctx);
let lv = level::get(&user_storage);

All storage operations emit standardized events that enable zero-code indexing and off-chain data synchronization.


Core storage model

Resources use two kinds of storage objects backed by Dubhe’s shared infrastructure:

Storage objectUsed forOne per
UserStoragePer-user data (default)User per DApp
DappStorageDApp-wide data (global: true)DApp

Generated resource modules accept the appropriate storage object directly — there is no resource_account string parameter. The storage object itself is the user or DApp identity.

UserStorage (user's personal store)
└── level, hp, inventory, …  (per-user resources)

DappStorage (DApp-wide store)
└── total_players, config, …  (global resources)

Concurrency model:

  • UserStorage scales linearly. Each user’s UserStorage is a separate shared object. Concurrent transactions from different users never block each other — throughput grows with the number of active users.
  • DappStorage writes serialize. There is only one DappStorage per DApp. Any transaction that takes &mut DappStorage must be sequenced through consensus. Use global: true only for infrequently updated data such as config flags or counters updated once per session. Avoid placing high-frequency write paths on global resources.

Resource patterns

There are six patterns depending on the combination of global, keys, and fields:

PatternConfig shapeStorage objectPrimary use case
Global singletonglobal: trueDappStoragePackage-level counters, config flags
User single value'MoveType' shorthandUserStorageSimple per-player properties
User multi-field recordfields onlyUserStorageStructs keyed by user
Keyed single valuefields + keys (1 value field)UserStorageMaps like (item_id) → quantity
Keyed multi-value recordfields + keys (multiple value fields)UserStorageRich indexed data
Offchainoffchain: trueUserStorageEvents only, no on-chain storage

See Config Reference for the full API generated by each pattern.


Enum and error support

Custom enum types can be declared alongside resources and used as field or key types. Each enum compiles to a Move module with constructors, matchers, and BCS helpers.

Custom error codes compile to assertion helpers that abort with a message string, making contract revert reasons immediately readable.


Why resources?

Resources address several hard problems in on-chain application development:

  • Separation of state from logic — business logic lives in system contracts; all state lives in storage objects. Upgrading logic never migrates data.
  • Object-level isolation — each user’s UserStorage is a separate shared object, so concurrent user transactions never contend on a single lock.
  • Automatic sync to frontends — all writes emit standardised events that a Dubhe indexer can consume without any extra code.
  • Cross-contract queries — any contract that receives a UserStorage can read data from that user’s store.
  • Gas efficiency — values are tightly packed in the underlying storage, often cheaper than hand-written table access.