Sending messages
TON Blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send messages.
Messages in Tact are commonly composed using a built-in Struct SendParameters
which consists of the following fields:
Field | Type | Description |
---|---|---|
mode | Int | An 8-bit value that configures how to send a message, defaults to . See: Message mode . |
body | Cell? | Optional message body as a Cell . |
code | Cell? | Optional initial code of the contract (compiled bitcode). |
data | Cell? | Optional initial data of the contract (arguments of init() function). |
value | Int | The amount of nanoToncoins you want to send with the message. This value is used to cover forward fees, unless the optional flag SendPayGasSeparately is used. |
to | Address | Recipient internal Address on TON Blockchain. |
bounce | Bool | When set to true (default) message bounces back to the sender if the recipient contract doesn’t exist or wasn’t able to process the message. |
Fields code
and data
are what’s called an init package, which is used in deployments of new contracts.
Send simple reply
The simplest message is a reply to the incoming message returning all excess value of a message:
self.reply("Hello, World!".asComment()); // asComment converts a String to a Cell with a comment
Send message
If you need more advanced logic you can use the send()
function and SendParameters
Struct directly.
In fact, the previous example with .reply()
can be made using the following call to send()
function:
send(SendParameters{ // bounce is set to true by default to: sender(), // sending message back to the sender value: 0, // don't add Toncoins to the message... mode: SendRemainingValue | SendIgnoreErrors, // ...except for ones received from the sender due to SendRemainingValue body: "Hello, World".asComment(), // asComment converts a String to a Cell with a comment});
Another example sends a message to the specified Address
with a value
of TON and the body
of a comment with a String
"Hello, World!"
:
let recipient: Address = address("...");let value: Int = ton("1");send(SendParameters{ // bounce is set to true by default to: recipient, value: value, mode: SendIgnoreErrors, // skip the message in case of errors body: "Hello, World!".asComment(),});
The optional flag SendIgnoreErrors
means that if an error occurs during message send, it will be ignored and the given message will be skipped. Message-related action phase exit codes that might be thrown without the SendIgnoreErrors
set are:
- :
Invalid destination address in outbound message
- :
Not enough Toncoin
- :
Outbound message doesn't fit into a cell
- :
Cannot process a message
Send typed message
To send a typed message you can use the following code:
let recipient: Address = address("...");let value: Int = ton("1");send(SendParameters{ // bounce is set to true by default to: recipient, value: value, mode: SendIgnoreErrors, // skip the message in case of errors body: SomeMessage{arg1: 123, arg2: 1234}.toCell(),});
Deploy contract
To deploy a contract you need to calculate its address and initial state with initOf
, then send them in the initialization message:
let init: StateInit = initOf SecondContract(arg1, arg2);let address: Address = contractAddress(init);let value: Int = ton("1");send(SendParameters{ // bounce is set to true by default to: address, value: value, mode: SendIgnoreErrors, // skip the message in case of errors code: init.code, data: init.data, body: "Hello, World!".asComment(), // not necessary, can be omitted});
For cheaper on-chain deployments prefer using the deploy()
function instead. It computes the address of the contract based on its initial code and data and efficiently composes the resulting message:
deploy(DeployParameters{ // bounce is set to true by default init: initOf SecondContract(arg1, arg2), // initial code and data mode: SendIgnoreErrors, // skip the message in case of errors value: ton("1"), // a whole Toncoin body: "Hello, World!".asComment(), // not necessary, can be omitted});
Outbound message processing
Each transaction on TON Blockchain consists of multiple phases. Outbound messages are evaluated in compute phase, but are not sent in that phase. Instead, they’re queued for execution in the action phase in order of their appearance in the compute phase.
Outgoing message sends may fail in action phase due to insufficient action fees or forward fees, in which case they won’t bounce and won’t revert the transaction. This can happen because all values are calculated in the compute phase, all fees are computed by the end of it, and exceptions do not roll back the transaction during the action phase.
To skip or ignore the queued messages at the action phase in case they cannot be sent, set the optional SendIgnoreErrors
flag when composing the message.
Consider the following example:
// This contract initially has 0 nanoToncoins on the balancecontract FailureIsNothingButAnotherStep { // And all the funds it gets are obtained from inbound internal messages receive() { // 1st outbound message evaluated and queued (but not yet sent) send(SendParameters{ to: sender(), value: ton("0.042"), // plus forward fee due to SendPayGasSeparately mode: SendIgnoreErrors | SendPayGasSeparately, // body is null by default });
// 2nd outbound message evaluated and queued, // but not yet sent, and never will be! send(SendParameters{ to: sender(), value: 0, mode: SendRemainingValue | SendIgnoreErrors, // body is null by default }); } // exit code 37 during action phase!}
There, the second message won’t actually be sent:
-
After finishing the compute phase, the remaining value of the contract is computed.
-
During the outbound message processing and assuming that there was enough value provided in the inbound message, the first message leaves nanoToncoins on the balance.
-
When the second message is processed, contract tries to send nanoToncoins, but fails to do so because there is already a smaller amount left.
-
Thus, an error with exit code 37 is thrown:
Not enough Toncoin
.
Note that such failures are not exclusive to the send()
function and may also occur when using other message-sending functions.
For instance, let’s replace the first call to the send()
function in the previous example with the emit()
function. The latter queues the message using the default mode, i.e. , and spends some nanoToncoins to pay the forward fees.
If a subsequent message is then sent with a SendRemainingValue
base mode, it will cause the same error as before:
// This contract initially has 0 nanoToncoins on the balancecontract IfItDiesItDies { // And all the funds it gets are obtained from inbound internal messages receive() { // 1st outbound message evaluated and queued (but not yet sent) // with the mode 0, which is the default emit("Have you seen this message?".asComment());
// 2nd outbound message evaluated and queued, // but not yet sent, and never will be! send(SendParameters{ to: sender(), value: 0, bounce: false, // brave and bold mode: SendRemainingValue, body: "Not this again!".asComment(), }); } // exit code 37 during action phase!}
Previous examples discussed a case where the contract has nanoToncoins on the balance, which is rather rare — in most real-world scenarios there would be some funds present. As such, it’s usually better to use the SendRemainingBalance
base mode, paired with the necessary call to the nativeReserve()
function.
Like outbound messages, reserve requests are queued during the compute phase and executed during the action phase.
// This contract has some Toncoins on the balance, say, 0.2 or morecontract MyPrecious { // The extra funds can be received via a "topup" message receive("topup") {}
// And the rest of the logic is expressed here receive() { // 1st outbound message evaluated and queued (but not yet sent) // with the mode 0, which is the default emit("Have you seen this message?".asComment());
// Let's try to keep the most out of the balance prior to this transaction // Notice that nativeReserve() only queues an action to be performed during action phase nativeReserve(ton("0.05"), ReserveAtMost | ReserveAddOriginalBalance); // ----------- ------------- ------------------------- // ↑ ↑ ↑ // | | keeping the balance from before the compute phase start // | might keep less, but won't fail in doing so // just a tad more on top of the balance, for the fees
// 2nd outbound message evaluated and queued // with the SendRemainingBalance mode send(SendParameters{ to: sender(), value: 0, mode: SendRemainingBalance, // because of the prior nativeReserve(), // using this mode is safe and we'll keep // the original balance plus a little more body: "I give you my all! Well, all that's not mine!".asComment(), }); }}
Instead, if you want all outgoing messages to preserve a fixed amount of funds on the balance and send the rest of the balance, consider using one of the following functions instead. Note that they require a prior override of the self.storageReserve
constant:
If you take only one thing away from this section, please remember this: be very careful with the base modes of the message-sending functions, including the implicitly set modes.
Message sending limits
In total, there could be no more than actions queued for execution, which means that the maximum allowed number of messages sent per transaction is .
Attempts to queue more throw an exception with an exit code 33 during action phase: Action list is too long
.
Message-sending functions
Read more about all message-sending functions in the Reference: