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.