Skip to content

Uniq On-chain Data

Overview of Uniq On-chain Data feature

Uniq On-chain Data is a flexible schema system that allows factory managers to define custom data structures for their Uniqs. It consists of two main components:

  1. Factory Key Definitions: Define the schema/structure with default values for all Uniqs from a factory
  2. Uniq Key Values: Store custom values that override factory defaults for individual Uniqs

The factory manager pays a non-refundable RAM fee when adding new key definitions. When a Uniq is minted, a RAM fee is transferred from the manager to their key RAM vault and refunded when the Uniq is burnt.

How the System Works

Factory Key Definitions

Factory key definitions are stored in token_factory_v1.keys and define the schema for all Uniqs from that factory:

cpp
struct factory_keys {
    vector<key_def_table> key_defs;          // Array of key definitions
    int64_t total_key_def_ram_payment_size;  // RAM costs
    int64_t total_key_value_ram_payment_size;
};

struct key_def_table {
    string name;                          // Key name (e.g. "Level", "Rarity")
    uint8_t type_index;                   // Index into supported types
    uint8_t edit_rights;                  // Who can edit this key
    vector<name> editors;                 // Authorized editors
    optional<key_value_store> default_value;  // Default value for all Uniqs
};

Key Properties:

  • Immutable Order: Existing key definitions cannot be modified or reordered
  • Append-Only: Only new keys can be added to the end
  • Default Values: Each key can specify a default value that all Uniqs inherit

Uniq Key Values

Individual Uniqs store custom values in token_v1.key_values to override factory defaults:

cpp
struct key_value_pair {
    uint8_t key_index;           // References factory key_defs array index
    key_value_store key_value;   // The custom value
};
typedef vector<key_value_pair> key_value_vec;
typedef map<uint8_t, key_value_store> key_value_map;

Key Properties:

  • Selective Storage: Uniqs only store values when overriding defaults
  • Index-Based References: Uses key_index to point to factory key definitions
  • Unordered: Custom values can be stored in any order
  • Optional: Uniqs may have no custom values (all defaults)

Integration Performance Note

For external integrators, the key_value_map structure can be used to read existing key_value_vector data:

  • Storage Format: On-chain data is stored as key_value_vector (for compatibility)
  • Reading Format: External contracts can deserialize as key_value_map for O(log n) lookups
  • Performance Trade-off: Map requires sorting overhead during deserialization but provides faster lookups
  • Use Case: Map approach may benefit contracts with many keys (>20) or very frequent lookups
  • Backward Compatibility: Existing contracts using vector format continue to work unchanged

Proper Integration Pattern

❌ WRONG - Hardcoded Array Indices

cpp
// DON'T DO THIS - Very fragile and will break!
string level = get<string>(uniq.key_values->value()[0].key_value);     // Assumes position 0
uint64_t rarity = get<uint64_t>(uniq.key_values->value()[1].key_value); // Assumes position 1

Why This is Wrong:

  • Assumes specific order in Uniq's key_values (but values can be in any order!)
  • Ignores default value system
  • Breaks when Uniqs store values in different orders
  • Doesn't follow the intended design pattern

✅ CORRECT - Name-Based Lookup with Defaults

cpp
// Proper pattern: Name → Factory Key Index → Uniq Value or Default
template<typename T>
T get_uniq_key_value(const token_v1& uniq, const factory_keys& factory_keys, const string& key_name) {
    // 1. Find key definition index by name
    uint8_t key_index = find_key_def_index(factory_keys.key_defs, key_name);
    const auto& key_def = factory_keys.key_defs[key_index];
    
    // 2. Search Uniq's key_values for custom value
    if (uniq.key_values && uniq.key_values->has_value()) {
        for (const auto& kv : uniq.key_values->value()) {
            if (kv.key_index == key_index) {
                return get<T>(kv.key_value);  // Found custom value
            }
        }
    }
    
    // 3. Fallback to factory default
    return get<T>(key_def.default_value.value());
}

// Usage
string level = get_uniq_key_value<string>(uniq, factory_keys, "Level");
uint64_t rarity = get_uniq_key_value<uint64_t>(uniq, factory_keys, "Rarity");

Alternative: Map-Based Integration for Specific Use Cases

For external integrators with many keys or very frequent lookups, you can deserialize the same data as a map:

cpp
// Using map structure for better performance
template<typename T>
T get_uniq_key_value_fast(const token_v1& uniq, const factory_keys& factory_keys, const string& key_name) {
    // 1. Find key definition index by name
    uint8_t key_index = find_key_def_index(factory_keys.key_defs, key_name);
    const auto& key_def = factory_keys.key_defs[key_index];
    
    // 2. Deserialize key_values as map for O(log n) lookup
    if (uniq.key_values && uniq.key_values->has_value()) {
        // Note: Same binary data, different deserialization
        key_value_map key_map = deserialize_as_map(uniq.key_values->value());
        
        auto it = key_map.find(key_index);
        if (it != key_map.end()) {
            return get<T>(it->second);  // Found custom value
        }
    }
    
    // 3. Fallback to factory default
    return get<T>(key_def.default_value.value());
}

// Usage - same API, better performance
string level = get_uniq_key_value_fast<string>(uniq, factory_keys, "Level");
uint64_t rarity = get_uniq_key_value_fast<uint64_t>(uniq, factory_keys, "Rarity");

Performance Analysis:

  • Vector approach: O(n) linear search, but no deserialization overhead
  • Map approach: O(log n) lookup, but requires sorting during deserialization
  • For typical use: Vector likely faster for ~10 keys or less due to sorting overhead
  • For large datasets: Map becomes beneficial with many keys (>20) or frequent lookups
  • Binary compatibility: Both read the same on-chain data

Data Flow Example

Factory Definition:
key_defs[0] = {name: "Level", default_value: "1"}
key_defs[1] = {name: "Rarity", default_value: "Common"}
key_defs[2] = {name: "Experience", default_value: 0}

Fresh Uniq (no custom values):
key_values = null
→ Level="1" (default), Rarity="Common" (default), Experience=0 (default)

Leveled Uniq (some custom values):
key_values = [
    {key_index: 0, key_value: "5"},      // Level overridden
    {key_index: 2, key_value: 1500}      // Experience overridden
]
→ Level="5" (custom), Rarity="Common" (default), Experience=1500 (custom)

Different Uniq (same data, different order):
key_values = [
    {key_index: 2, key_value: 1500},     // Experience overridden
    {key_index: 0, key_value: "5"}       // Level overridden
]
→ Functionally identical to previous Uniq

Integration Best Practices

1. Always Use Name-Based Lookup

cpp
// ✅ Good - Resilient to changes
auto value = get_uniq_key_value<string>(uniq, factory_keys, "AttributeName");

// ✅ Alternative - For high-scale scenarios with many keys or frequent lookups
auto value = get_uniq_key_value_fast<string>(uniq, factory_keys, "AttributeName");

// ❌ Bad - Fragile and will break
auto value = get<string>(uniq.key_values->value()[0].key_value);

2. Choose Integration Approach Based on Use Case

cpp
// ✅ Vector approach - Recommended for typical use cases
// Use when: ~10 keys or less, occasional lookups, simple integration
auto value = get_uniq_key_value<string>(uniq, factory_keys, "AttributeName");

// ✅ Map approach - For specific high-scale scenarios
// Use when: Many keys (>20), very frequent lookups, large datasets
auto value = get_uniq_key_value_fast<string>(uniq, factory_keys, "AttributeName");

3. Handle Default Values Properly

cpp
// ✅ Your lookup function should automatically:
// 1. Check if Uniq has custom value
// 2. If not found, use factory default
// 3. Error if neither exists

4. Validate Factory Schema Once

cpp
// ✅ At function start, validate factory has expected keys
void validate_factory_keys(const factory_keys& keys, const vector<string>& expected) {
    for (const auto& expected_key : expected) {
        bool found = false;
        for (const auto& key_def : keys.key_defs) {
            if (key_def.name == expected_key) {
                found = true;
                break;
            }
        }
        check(found, "Factory missing expected key: " + expected_key);
    }
}

5. Understand Business Logic vs Data Access

cpp
// Business logic validation (workflow states)
check(!uniq.key_values->has_value(), "uniq should be fresh");     // Before processing
check(uniq.key_values->has_value(), "uniq should be processed");  // After processing

// Data access (use proper lookup)
string status = get_uniq_key_value<string>(uniq, factory_keys, "Status");

Common Integration Mistakes

❌ Mistake 1: Assuming All Values Are Present

cpp
// Wrong - assumes Uniq has all values stored
for (int i = 0; i < factory_keys.key_defs.size(); i++) {
    auto value = uniq.key_values->value()[i].key_value;  // Will crash!
}

❌ Mistake 2: Ignoring Order Independence

cpp
// Wrong - assumes specific order
string first_attr = get<string>(uniq.key_values->value()[0].key_value);   // Could be anything!
string second_attr = get<string>(uniq.key_values->value()[1].key_value);  // Could be anything!

❌ Mistake 3: Not Handling Fresh Uniqs

cpp
// Wrong - crashes on fresh Uniqs with no custom values
check(uniq.key_values->has_value(), "missing key values");  // Shouldn't be required for reading!

Actions Reference

Benefits of Uniq On-chain data

  • Dynamic Metadata: Allow Uniq's metadata to be updated on chain without off-chain complexity

  • Marketplace Integration: Enable Uniq marketplaces to provide filters for Uniqs with changing data

  • Game Mechanics: Support evolving game assets with stats, levels, and attributes

  • Flexible Schema: Define custom data structures that fit your specific use case

  • Storage Efficiency: Uniqs only store values that differ from factory defaults

Summary

Uniq On-chain Data is a powerful flexible schema system, not a fixed data structure. Proper integration requires:

  1. Name-based key lookup instead of position-based access
  2. Appropriate performance approach - vector for typical use cases, map for high-scale scenarios
  3. Default value fallback for efficient storage
  4. Order independence awareness for robust code
  5. Business logic separation from data access patterns

Integration Options:

  • Vector approach: Simple, direct deserialization - recommended for typical use cases (~10 keys or less)
  • Map approach: Same data with sorted structure - beneficial for large datasets (>20 keys) or very frequent lookups
  • Binary compatibility: Both approaches read identical on-chain data

Following these patterns ensures your integration is robust, maintainable, and won't break as the system evolves.