Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: clarify interactions of message sending functions and their modes #1634

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Document how storage variables get updated in relation to the `init()` function: PR [#1311](https://github.com/tact-lang/tact/pull/1311)
- Document compiler upgrades in Blueprint and other Tact projects: PR [#1560](https://github.com/tact-lang/tact/pull/1560)
- Illustrate how nested maps can be created: PR [#1593](https://github.com/tact-lang/tact/pull/1593)
- Listed functions with implicit mode and further clarified the interactions of message sending functions and their modes: PR [#1634](https://github.com/tact-lang/tact/pull/1634)

### Release contributors

Expand Down
11 changes: 7 additions & 4 deletions docs/src/content/docs/book/message-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,19 @@ send(SendParameters{
});
```

Note that there can be only **one** [base mode](#base-modes), but number of [optional flags](#optional-flags) may vary: you can use them all, none or just some.

:::caution

Note, that while adding ([`+{:tact}`](/book/operators#binary-add)) base modes together with optional flags is possible, it is discouraged due to the possibility of excess values. Use the bitwise OR ([`|{:tact}`](/book/operators#binary-bitwise-or)) instead, as it's designed to work with such flag and bit manipulations of the `mode`.
While adding ([`+{:tact}`](/book/operators#binary-add)) base modes together with optional flags is possible, it is discouraged due to the possibility of excess values. Use the [bitwise OR `|{:tact}`](/book/operators#binary-bitwise-or) instead, as it's designed to work with such flag and bit manipulations of the `mode`.

:::

:::note
## Functions with implicit mode

Also note, that there can be only one [base mode](#base-modes), but number of [optional flags](#optional-flags) may vary: you can use them all, none or just some.
Some [message sending functions](/book/send#message-sending-functions) do not allow to set a mode by passing an argument. That's because their internal logic requires specific fixed set of modes to be used instead:

:::
* [`emit(){:tact}`](/ref/core-common#emit) sends a message with the `SendDefaultMode{:tact}` ($0$).
* [`self.reply(){:tact}`](/ref/core-base#self-reply), [`self.notify(){:tact}`](/ref/core-base#self-notify), and [`self.forward(){:tact}`](/ref/core-base#self-forward) all use the `SendRemainingValue{:tact}` mode unless the [`self.storageReserve{:tact}`](/ref/core-base#self-storagereserve) constant is overwritten to be greater than $0$, in which case they attempt to use the `SendRemainingBalance{:tact}` mode.

[int]: /book/integers
52 changes: 46 additions & 6 deletions docs/src/content/docs/book/send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Sending messages
description: "TON Blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send messages."
---

TON blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send 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](/book/structs-and-messages#structs) `SendParameters{:tact}`, which consists of:

Expand Down Expand Up @@ -112,30 +112,69 @@ Consider the following example:
contract FailureIsNothingButAnotherStep {
// And all the funds it gets are obtained from inbound internal messages
receive() {
// 1st outbound message evaluated and queued (but not sent yet)
// 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 sent yet, and never will be!)
// 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][compute], the remaining value $\mathrm{R}$ 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 $\mathrm{R} - (0.042 + \mathrm{forward\_fees})$ [nanoToncoins](/book/integers#nanotoncoin) on the balance.
* During the outbound message processing and assuming that there was enough value provided in the inbound message, the first message leaves $\mathrm{R} - (0.042 + \mathrm{forward\_fees})$ [nanoToncoins][nano] on the balance.

* When the second message is processed, contract tries to send $\mathrm{R}$ [nanoToncoins](/book/integers#nanotoncoin), but fails to do so because there is already a smaller amount left.
* When the second message is processed, contract tries to send $\mathrm{R}$ [nanoToncoins][nano], but fails to do so because there is already a smaller amount left.

* Thus, an error with [exit code 37](/book/exit-codes#37) is thrown: `Not enough Toncoin`.

Note that such failures are not exclusive to the [`send(){:tact}`](/ref/core-common#send) function and may also occur when using other [message sending functions](#message-sending-functions).

For instance, let's replace the first call to the [`send(){:tact}`](/ref/core-common#send) function in the previous example with the [`emit(){:tact}`](/ref/core-common#emit) function. The latter queues the message using the default mode, i.e. $0$, and spends some [nanoToncoins][nano] to pay the [forward fees][fwdfee].

If a subsequent message is then sent with a [`SendRemainingValue{:tact}`](/book/message-mode#base-modes) base mode, it will cause the same error as before:

```tact
// This contract initially has 0 nanoToncoins on the balance
contract 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!
}
```

:::note

To avoid dealing with similar cases and to simplify future [debugging sessions](/book/debug), consider having only one call to one of the [message sending functions](#message-sending-functions) per [receiver function](/book/receive), preferably at the end of the function body.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should suggest a different solution based on nativeReserve here. @Shvandre can you please help with this part

Copy link
Contributor

@Shvandre Shvandre Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this specific case (when contract initially has 0 on balance). Such code will work properly

send(SendParameters{
    bounce: false,
    body: "TEST1".asComment(),
    to: sender(), //Actually any address
    value: ton("0.5"),
    mode: SendDefaultMode,
});

send(SendParameters{
    bounce: false,
    body: "TEST2".asComment(),
    to: myAddress(), //Actually any address
    value: 0,
    mode: SendRemainingBalance,
});

And nativeReserve is unnecessary here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is in bounce: false and using SendRemainingBalance instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, bounce does not affect anything.
Point is that SendRemainingBalance works correctly, taking previous actions into account,
while SendRemainingValue does not

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! super-useful, thanks Andrei

let's add a solution what to do when the balance is not zero

Copy link
Member Author

@novusnota novusnota Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, and I'll also add info on SendRemainingValue vs. SendRemainingBalance to the book/message-mode, under the table in "Base modes" section


:::

## Message sending limits

Expand All @@ -158,6 +197,7 @@ Read more about all message sending functions in the Reference:
[int]: /book/integers
[cell]: /book/cells#cells
[opt]: /book/optionals
[nano]: /book/integers#nanotoncoin

[phases]: https://docs.ton.org/learn/tvm-instructions/tvm-overview#transactions-and-phases
[compute]: https://docs.ton.org/learn/tvm-instructions/tvm-overview#compute-phase
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/ref/core-base.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ virtual fun forward(to: Address, body: Cell?, bounce: Bool, init: StateInit?);

[Queues the message](/book/send#outbound-message-processing) (bounceable or non-bounceable) to be sent to the specified address `to`. Optionally, you may provide a `body` of the message and the [`init` package](/book/expressions#initof).

When [`self.storageReserve{:tact}`](#self-storagereserve) constant is overwritten to be $> 0$, before sending a message it also tries to reserve the `self.storageReserve{:tact}` amount of [nanoToncoins][nano] from the remaining balance before making the send in the [`SendRemainingBalance{:tact}`](https://docs.tact-lang.org/book/message-mode#base-modes) ($128$) mode.
When [`self.storageReserve{:tact}`](#self-storagereserve) constant is overwritten to be greater than $0$, before sending a message it also tries to reserve the `self.storageReserve{:tact}` amount of [nanoToncoins][nano] from the remaining balance before making the send in the [`SendRemainingBalance{:tact}`](/book/message-mode#base-modes) ($128$) mode.

In case reservation attempt fails and in the default case without the attempt, the message is sent with the [`SendRemainingValue{:tact}`](https://docs.tact-lang.org/book/message-mode#base-modes) ($64$) mode instead.
In case reservation attempt fails and in the default case without the attempt, the message is sent with the [`SendRemainingValue{:tact}`](/book/message-mode#base-modes) ($64$) mode instead.

:::note

Note, that `self.forward(){:tact}` never sends additional [nanoToncoins][nano] on top of what's available on the balance.\
To be able to send more [nanoToncoins][nano] with a single message, use the the [`send(){:tact}`](/ref/core-common#send) function.
To be able to send more [nanoToncoins][nano] with a single message, use the [`send(){:tact}`](/ref/core-common#send) function.

:::

Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/ref/core-common.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ send(SendParameters{
fun emit(body: Cell);
```

[Queues the message](/book/send#outbound-message-processing) `body` to be sent to the outer world with the purpose of logging and analyzing it later off-chain. The message does not have a recipient and is gas-efficient compared to using any other message sending functions of Tact.
[Queues the message](/book/send#outbound-message-processing) `body` to be sent to the outer world with the purpose of logging and analyzing it later off-chain. The message does not have a recipient and is more gas-efficient compared to using any other [message sending functions](/book/send#message-sending-functions) of Tact.

The message is sent with the default mode: [`SendDefaultMode`](/book/message-mode#base-modes) ($0$).

Attempts to queue more than $255$ messages throw an exception with an [exit code 33](/book/exit-codes#33): `Action list is too long`.

Expand Down