Handling messages
Each Tolk contract has special entrypoints – reserved functions that handle different message types. Handling an incoming message uses ordinary language constructs.
A contract file typically begins with a contract declaration that names the contract and lists its public shapes — including the union of incoming messages.
onInternalMessage
Contracts primarily handle internal messages. Users interact with contracts through their wallets, which send internal messages to the contract. The entrypoint is declared as follows:
fun onInternalMessage(in: InMessage) {
// internal non-bounced messages arrive here
}The basic guidelines are:
- For each incoming message, declare a
structwith a unique 32-bit prefix, opcode. - Declare a union type that represents all supported messages.
- Reference the union from
incomingMessages:in thecontractdeclaration. - Parse this union from
in.bodyandmatchover structures.
struct (0x12345678) CounterIncrement {
incBy: uint32
}
struct (0x23456789) CounterReset {
initialValue: int64
}
type AllowedMessage = CounterIncrement | CounterReset
contract Counter {
storage: Storage
incomingMessages: AllowedMessage
}
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessage.fromSlice(in.body);
match (msg) {
CounterIncrement => {
// use `msg.incBy`
}
CounterReset => {
// use `msg.initialValue`
}
else => {
// invalid input; a typical reaction is:
// ignore empty messages, "wrong opcode" if not
assert (in.body.isEmpty()) throw 0xFFFF
}
}
}Example breakdown
structdeclares business data, including messages and storage.(0x12345678)defines a message opcode, 32-bit. Unique prefixes are used to route binary data inin.body.AllowedMessageis a type alias for a union type.in: InMessageprovides access to message properties such asin.bodyandin.senderAddress.T.fromSliceparses binary data intoT. When combined withlazy, parsing is performed on demand.matchroutes a union type. Within each branch, the type ofmsgis narrowed, smart cast.throw 0xFFFFis a standard reaction to an unrecognized message. Contracts typically ignore empty messages, which represent balance top-ups with an empty body. For this reason,throwis guarded byiforassert.
Bounced messages are not handled by onInternalMessage.
Define and modify contract storage
Contract storage is defined as a regular structure. Storage types commonly define load and save methods to access persistent contract data:
struct Storage {
counterValue: int64
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun Storage.save(self) {
contract.setData(self.toCell())
}Then, in match cases, invoke those methods:
match (msg) {
CounterIncrement => {
var storage = lazy Storage.load();
storage.counterValue += msg.incBy;
storage.save();
}
// ...
}Storage may also be loaded once before the match statement and reused across branches.
Legacy onInternalMessage
In FunC, a handler is declared as:
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; manually parse in_msg_full to retrieve sender_address and others
}Tolk continues to support this style of declaration. Code produced by a converter results in:
fun onInternalMessage(myBalance: coins, msgValue: coins, msgFull: cell, msgBody: slice) {
// manually parse msgFull to retrieve senderAddress and others
}The modern approach uses the InMessage type. It simplifies message handling and reduces gas consumption. Migrating from the legacy code is:
myBalance->contract.getOriginalBalance(), contract state, not a message propertymsgValue->in.valueCoinsmsgFull-> usein.senderAddressetc., without manual parsingmsgBody->in.body
onBouncedMessage
onBouncedMessage is a special entrypoint for handling bounced messages.
fun onBouncedMessage(in: InMessageBounced) {
// messages sent with BounceMode != NoBounce arrive here
}InMessageBounced is similar to InMessage. The difference is that in.bouncedBody has a different layout, depending on how the original message is sent.
BounceMode in createMessage
When sending a message using createMessage, the bounce behavior must be specified:
val msg1 = createMessage({
bounce: BounceMode.NoBounce,
body: TransferMessage { ... },
// ...
});
msg1.send(mode); // will not be bounced on error
val msg2 = createMessage({
bounce: BounceMode.RichBounce,
body: TransferMessage { ... },
// ...
});
msg2.send(mode); // may be bouncedBounceMode is an enum with the following options:
BounceMode.NoBounce.BounceMode.Only256BitsOfBody—in.bouncedBodycontains0xFFFFFFFFfollowed by the first 256 bits; lowest gas cost, often sufficient.BounceMode.RichBounce— provides access to the entireoriginalBody;gasUsed,exitCode, and other failure-related properties are also available; highest gas cost.BounceMode.RichBounceOnlyRootCell— similar toRichBounce, butoriginalBodycontains only the root cell.
Handle in.bouncedBody
The structure of in.bouncedBody depends on the BounceMode.
When all bounceable messages are sent using Only256BitsOfBody:
fun onBouncedMessage(in: InMessageBounced) {
// in.bouncedBody is 0xFFFFFFFF + 256 bits
in.bouncedBody.skipBouncedPrefix();
// handle the rest, keep the 256-bit limit in mind
}When RichBounce is used:
fun onBouncedMessage(in: InMessageBounced) {
val rich = lazy RichBounceBody.fromSlice(in.bouncedBody);
// handle rich.originalBody
// use rich.xxx to get exitCode, gasUsed, and so on
}Mixing different modes, where some messages use a minimal body and others use a full body, complicates handling and is discouraged. The binary body of an outgoing message, such as TransferMessage, is returned either as in.bouncedBody with a 256-bit limit or as rich.originalBody, which contains the full slice. To handle this consistently:
- define a union type that includes all message types that may be bounced;
- handle it using
lazyinonBouncedMessage.
struct (0x98765432) TransferMessage {
// ...
}
// ... and other messages
// some of them are bounceable (send not with NoBounce)
type TheoreticallyBounceable = TransferMessage // | ...
// example for BounceMode.Only256BitsOfBody
fun onBouncedMessage(in: InMessageBounced) {
in.bouncedBody.skipBouncedPrefix(); // skips 0xFFFFFFFF
val msg = lazy TheoreticallyBounceable.fromSlice(in.bouncedBody);
match (msg) {
TransferMessage => {
// revert changes using `msg.xxx`
}
// ...
}
}onExternalMessage
In addition to internal messages, a contract can handle external messages originating off-chain. For example, wallet contracts process external messages and perform signature validation using a public key.
fun onExternalMessage(inMsg: slice) {
// external messages arrive here
}When a contract accepts an external message, it has limited gas for execution. After validating the request, the contract must call acceptExternalMessage() to increase the available gas. The commitContractDataAndActions() function can also be used. Both functions are part of the standard library and are documented inline.
Additional reserved entrypoints
Tolk defines several reserved entrypoints:
fun onRunTickTockis invoked on tick-tock transactions;fun onSplitPrepareandfun onSplitInstallare reserved for split and install transactions; currently not used by the blockchain;fun mainis used for simple snippets and demos.
The following program is valid:
fun main() {
return 123
}It compiles and runs, pushing value 123 onto the stack. The corresponding TVM method_id is 0.
Last updated on