Хранение данных в контракте: структуры и мапы

Спроектируешь storage на struct и map в Tact: вложенные данные, словари и типичные ограничения размера.

Struct и map в storage

Постоянные данные контракта лежат в одной data-cell (с возможными ref). В Tact для составных значений используют struct, для ключ→значение — map<K, V> (словарь / hashmap в ячейках).


Реестр с профилями

tact (fallback highlight)
// @tact-lang/compiler 1.6.13

struct Profile {
    nameHash: Int as uint256;
    score: Int as uint32;
    active: Bool;
}

message Upsert {
    user: Address;
    nameHash: Int as uint256;
    score: Int as uint32;
}

message SetActive {
    user: Address;
    active: Bool;
}

contract Scoreboard(
    owner: Address,
    profiles: map<Address, Profile>,
    totalActive: Int as uint32,
) {
    receive() {
        cashback(sender());
    }

    receive(msg: Upsert) {
        require(sender() == self.owner, "not owner");
        let prev: Profile? = self.profiles.get(msg.user);
        let wasActive: Bool = prev != null && prev!!.active;

        let next: Profile = Profile{
            nameHash: msg.nameHash,
            score: msg.score,
            active: prev != null ? prev!!.active : true,
        };
        self.profiles.set(msg.user, next);

        if (!wasActive && next.active) {
            self.totalActive += 1;
        }
        cashback(sender());
    }

    receive(msg: SetActive) {
        require(sender() == self.owner, "not owner");
        let prev: Profile? = self.profiles.get(msg.user);
        require(prev != null, "unknown user");

        if (prev!!.active != msg.active) {
            self.totalActive += msg.active ? 1 : -1;
        }
        self.profiles.set(msg.user, Profile{
            nameHash: prev!!.nameHash,
            score: prev!!.score,
            active: msg.active,
        });
        cashback(sender());
    }

    get fun totalActive(): Int {
        return self.totalActive;
    }

    get fun profileOf(user: Address): Profile? {
        return self.profiles.get(user);
    }
}

Как это работает

  • struct Profile — именованный набор полей; сериализуется компактно, удобно класть в map.
  • map<Address, Profile> — словарь в storage; get возвращает T?, set пишет/перезаписывает.
  • Агрегат totalActive кэширует то, что дорого считать полным сканом map on-chain.
  • require(sender() == self.owner, ...) — без проверки владельца любой сможет переписать реестр.
  • Getters могут возвращать Profile? — UI узнаёт, есть ли ключ, без отдельного exists.

Частые ошибки

Класть огромный struct целиком в одно сообщение и в map без лимитов → упрёшься в лимиты cell (1023 бита / 4 ref) и газ.

Считать размер map полным перебором в receiver → нет дешёвого «length» для больших словарей; храни счётчики рядом.

Забыть обновить денормализованное поле (totalActive) → getter врёт, хотя map уже другой.


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация