Skip to main content

Documentation Index

Fetch the complete documentation index at: https://companyname-a7d5b98e-jeshecdom-fift-variables.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Contract storage is not “something special”. It is a regular struct, serialized into persistent blockchain data. Tolk does not impose strict rules, although several common guidelines are helpful in practice.

Common pattern: Storage, load(), and save()

A storage a regular structure. It is convenient to add load and store methods:
struct Storage {
    counterValue: int64
}

fun Storage.load() {
    return Storage.fromCell(contract.getData())
}

fun Storage.save(self) {
    contract.setData(self.toCell())
}
Then, at any point in a program, it can be easily accessed or modified:
get fun currentCounter() {
    var storage = lazy Storage.load();
    return storage.counterValue;
}

fun demoModify() {
    var storage = lazy Storage.load();
    storage.counterValue += 100;
    storage.save();
}
Concepts used:
  • struct behaves similarly to a TypeScript class. See structures.
  • fun Storage.f(self) defines an instance method. See functions.
  • T.fromCell() deserializes a cell into T, and obj.toCell() packs it back into a cell. See automatic serialization.
  • lazy operator does this parsing on demand. See lazy loading.
  • contract.getData() fetches persistent data. See standard library.

Set default values to fields

In TON, the contract’s address depends on its initial storage, when a contract is created on-chain. A good practice is to assign default values to fields that must have defined values at deployment:
struct WalletStorage {
    // these fields must have these values when deploying
    // to make the contact's address predictable
    jettonBalance: coins = 0
    isFrozen: bool = false

    // these fields must be manually assigned for deployment
    ownerAddress: address
    minterAddress: address
}
Therefore, to calculate an initial address, only two fields are required to be provided.

Multiple contracts in a project

When developing multiple contracts simultaneously (for example, a jetton minter and a jetton wallet), every contract has its own storage shape described by a struct. Give these structs reasonable names — for example, MinterStorage and WalletStorage. It’s better to place them in a single file (storage.tolk) together with their methods. Contracts often deploy each other, and initial storage must be provided during deployment. For example, a minter deploys a wallet, so WalletStorage becomes accessible via a simple import:
// all symbols from imported files become visible
import "storage"

fun deploy(ownerAddress: address, minterAddress: address) {
    val emptyWalletStorage: WalletStorage = {
        ownerAddress,
        minterAddress,
        // the other two use their defaults
    };
    // ...
}
See sending messages for examples of deployment.

Storage that changes its shape

Another pattern for address calculation and for security is:
  • when a contract is deployed, it has fields a,b,c (uninitialized storage)
  • followed by a message supplying d,e — it becomes a,b,c,d,e
Such patterns are common in NFTs. Initially, an NFT has only itemIndex and collectionAddress, nothing more (an uninitialized NFT). Upon initialization, fields ownerAddress and content are appended to a storage. How can such logic be implemented? Since arbitrary imperative code is allowed, the suggested approach is:
  • describe two structures: “initialized” and “uninitialized” storage
  • start loading contract.getData()
  • detect whether storage is initialized based on its bits/refs counts
  • parse into one or another struct
A long demo with detailed comments:
// two structures representing different storage states

struct NftItemStorage {
    itemIndex: uint64
    collectionAddress: address
    ownerAddress: address
    content: cell
}

struct NftItemStorageNotInitialized {
    itemIndex: uint64
    collectionAddress: address
}

// instead of the usual `load()` method — `startLoading()`

fun NftItemStorage.startLoading() {
    return NftItemStorageLoader.fromCell(contract.getData())
}

fun NftItemStorage.save(self) {
    contract.setData(self.toCell())
}

// this helper detects shape of a storage
struct NftItemStorageLoader {
    itemIndex: uint64
    collectionAddress: address
    private rest: RemainingBitsAndRefs
}

// when `rest` is empty, `collectionAddress` is the last field
fun NftItemStorageLoader.isNotInitialized(self) {
    return self.rest.isEmpty()
}

// `endLoading` continues loading when `rest` is not empty
fun NftItemStorageLoader.endLoading(mutate self): NftItemStorage {
    return {
        itemIndex: self.itemIndex,
        collectionAddress: self.collectionAddress,
        ownerAddress: self.rest.loadAny(),
        content: self.rest.loadAny(),
    }
}
Usage in onInternalMessage:
var loadingStorage = NftItemStorage.startLoading();
if (loadingStorage.isNotInitialized()) {
    // ... probably, initialize and save
    return;
}

var storage = loadingStorage.endLoading();
// and the remaining logic: lazy match, etc.