Universal createMessage
In FunC, you had to compose message cells manually and regularly face code like:
cell m = begin_cell()
.store_uint(0x18, 6)
.store_slice(sender_address)
.store_coins(50000000)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(0x178d4519, 32)
.store_uint(query_id, 64)
...
.end_cell();
send_raw_message(m, 0);
In Tolk, you use a high-level function — and it's even more gas-effective:
val reply = createMessage({
bounce: true,
value: ton("0.05"),
dest: senderAddress,
body: RequestedInfo { ... }
});
reply.send(SEND_MODE_REGULAR);
Key features of createMessage
- Supports extra currencies
- Supports
stateInit
(code+data) with automatic address computation - Supports different WorkChains
- Supports sharding (formerly splitDepth)
- Integrated with auto-serialization of
body
- Automatically detects body ref or not
- More efficient than handwritten code
The concept is based on union types
There is a huge variety of interacting between contracts. When you explore FunC implementations, you notice that:
- sometimes, you "send to an address (slice)"
- ... but sometimes, you "build the address (builder) manually"
- sometimes, you compose
StateInit
from code+data - ... but sometimes, you already have
StateInit
as a ready cell - sometimes, you send a message to basechain
- ... but sometimes, you have the
MY_WORKCHAIN
constant and use it everywhere - sometimes, you just attach tons (msg value)
- ... but sometimes, you also need extra currencies
- etc.
How can we describe such a vast variety of options? The solution is union types!
Let's start exploring this idea by looking at how extra currencies are supported.
Extra currencies: union
When you don't need them, you just attach msg value as tons:
value: someTonAmount
When you need them, you attach tons AND extra currencies dict:
value: (someTonAmount, extraDict)
How does it work? Because the field value
is a union:
// how it is declared in stdlib
type ExtraCurrenciesDict = dict;
struct CreateMessageOptions<TBody> {
...
/// message value: attached tons (or tons + extra currencies)
value: coins | (coins, ExtraCurrenciesDict)
That's it! You just attach tons OR tons with extra, and the compiler takes care of composing this into a cell.
Destination: union
The same idea of union types spreads onto destination of a message.
dest: someAddress,
dest: (workchain, hash)
It's either an address, OR a OR (WorkChain + hash), OR ...:
struct CreateMessageOptions<TBody> {
...
/// destination is either a provided address, or is auto-calculated by stateInit
dest: address | // either just send a message to some address
builder | // ... or a manually constructed builder with a valid address
(int8, uint256) | // ... or to workchain + hash (also known as accountID)
AutoDeployAddress; // ... or "send to stateInit" aka deploy (address auto-calculated)
That's indeed the TypeScript way — but it works at compile-time!
StateInit and WorkChains
Let's start from an example. From a jetton minter, you are deploying a jetton wallet. You know wallet's code and initial data:
val walletInitialState: ContractState = {
code: ..., // probably, kept in minter's storage
data: ..., // zero balance, etc. (initial wallet's storage)
};
Now, from a minter, you want to send a message to a wallet. But since you are not sure whether the wallet already exists onchain, you attach its code+data:
if a wallet doesn't exist, it's immediately initialized with that code.
So, where should you send a message to? What is destination? The answer is: destination is the wallet's StateInit.
You need to send a message to a walletInitialState
because, in TON, the address of a contract is — by definition — a hash of its initial state:
// address auto-calculated, code+data auto-attached
dest: {
stateInit: walletInitialState
}
In more complex tasks, you can configure additional fields:
dest: {
workchain: ..., // by default, 0 (basechain)
stateInit: ..., // either code+data OR a ready cell
toShard: ..., // by default, null (no sharding)
}
That's the essence of AutoDeployAddress
. Here is how it's declared in stdlib:
// declared in stdlib
struct AutoDeployAddress {
workchain: int8 = BASECHAIN;
stateInit: ContractState | cell;
toShard: AddressShardingOptions? = null;
}
Sharding: deploying "close to" another contract
The createMessage
interface also supports initializing contracts in specific shards. Say you're writing sharded jettons, and you want every jetton wallet to be in the same shard as the owner's wallet.
In other words, your intention is:
- a jetton wallet must be close to the owner's wallet
- this closeness is determined by a shard depth (syn. fixed prefix length, syn. split depth)
Let's illustrate it with numbers for shard depth
= 8:
Title | Addr hash (256 bits) | Comment |
---|---|---|
closeTo (owner addr) | 01010101...xxx | owner's wallet |
shardPrefix | 01010101 | first 8 bits of closeTo |
stateInitHash | yyyyyyyy...yyy | calculated by code+data |
result (JW addr) | 01010101...yyy | jetton wallet in same shard as owner |
That's how you do it:
dest: {
stateInit: walletInitialState,
toShard: {
closeTo: ownerAddress,
fixedPrefixLength: 8
}
}
Technically, shard depth must be a part of StateInit
(besides code+data) — for correct initialization inside the blockchain.
The compiler automatically embeds it.
But semantically, shard depth alone makes no sense. That's why shard depth + closeTo is a single entity:
// how it is declared in stdlib
struct AutoDeployAddress {
...
toShard: AddressShardingOptions? = null;
}
struct AddressShardingOptions {
fixedPrefixLength: uint5; // shard depth, formerly splitDepth
closeTo: address;
}
Body ref or not: compile-time calculation
In TON Blockchain, according to the specification, a message is a cell (flags, dest address, stateInit, etc.), and its body can be either inlined into the same cell or can be placed into its own cell (and be a ref).
In FunC, you had to manually calculate whether it's safe to embed body (you did it on paper or dynamically).
In Tolk, you just pass body
, and the compiler does all calculations for you:
createMessage({
...
body: RequestedInfo { ... } // no `toCell`! just pass an object
});
The rules are the following:
- if
body
is small, it's embedded directly into a message cell - if
body
is large or unpredictable, it's wrapped into a ref
Why not make a ref always? Because creating cells is expensive. Avoiding cells for small bodies is crucial for gas consumption.
Interestingly, whether the body is small is determined AT COMPILE TIME — no runtime checks are needed.
How? Thanks to generics! Here’s how createMessage
is declared:
fun createMessage<TBody>(options: CreateMessageOptions<TBody>): OutMessage;
struct CreateMessageOptions<TBody> {
...
body: TBody;
}
Hence, when you pass body: RequestedInfo {...}
, then TBody = RequestedInfo
, and the compiler estimates its size:
- it's small if its maximum size is less than 500 bits and 2 refs — then no ref
- it's large if >= 500 bits or >= 2 refs — then "ref"
- it's unpredictable if contains
builder
orslice
inside — then ref
Even if body is large/unpredictable, you can force it to be inlined by wrapping into a special type:
// potentialy 620 bits (if all coins are billions of billions)
// by default, compiler will make a ref
struct ProbablyLarge {
a: (coins, coins, coins, coins, coins)
}
val contents: ProbablyLarge = { ... }; // but you are sure: coins are small
createMessage({ // so, you take the risks
body: UnsafeBodyNoRef { // and force "no ref"
forceInline: contents,
}
// here TBody = UnsafeBodyNoRef<ProbablyLarge>
If body
is already a cell, it will be left as a ref, without any surprise:
createMessage({
body: someCell, // ok, just a cell, keep it as a ref
// here TBody = cell
That's why, don't pass body: someObj.toCell()
, pass just body: someObj
, let the compiler take care of everything.
Body is not restricted to structures
In practice, you use createMessage
to send a message (sic!) to another contract — in the exact format as the receiver expects.
You declare a struct with 32-bit opcode and some data in it.
struct (0xd53276db) Excesses {
queryId: uint64;
}
val excessesMsg = createMessage({
...
body: Excesses {
queryId: input.queryId,
}
});
excessesMsg.send(SEND_MODE_IGNORE_ERRORS);
This works perfectly, as expected. But an interesting fact—this also works:
// just an example, that even this would work
val excessesMsg = createMessage({
...
body: (0xd53276db as int32, input.queryId)
});
excessesMsg.send(SEND_MODE_IGNORE_ERRORS);