Skip to content

Commit e078ce7

Browse files
authored
feat(docs): refunds, message chains, and the carry-value pattern (#3422)
1 parent 5b7f81f commit e078ce7

File tree

3 files changed

+85
-7
lines changed

3 files changed

+85
-7
lines changed

dev-docs/CHANGELOG-DOCS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
77
## Doc: 2025-06
88

99
- Adjusted inline code tag highlighting to support global Starlight themes, and modified the One Light color theme to support proper highlighting of `keyword.operator.new` TextMate scopes: PR [#3346](https://github.com/tact-lang/tact/pull/3346)
10-
- Warned that imports are automatically exported: PR [#TBD](https://github.com/tact-lang/tact/pull/TBD)
10+
- Warned that imports are automatically exported: PR [#3368](https://github.com/tact-lang/tact/pull/3368)
11+
- Documented refunds, message chains, and the carry-value pattern for receivers: PR [#3422](https://github.com/tact-lang/tact/pull/3422)
1112

1213
## Doc: 2025-05
1314

docs/src/content/docs/book/contracts.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ contract Example {
244244

245245
:::note
246246

247-
Keep in mind that contracts cannot call each other's [getters](#getter-functions) or retrieve data from other contracts synchronously. Thus, as contracts exchange asynchronous messages and it is not possible to directly read state variables of one contract by another, because by the time a message with the data reaches your contract, the state variables on another contract might have changed already.
247+
Keep in mind that contracts cannot call each other's [getters](#getter-functions) or retrieve data from other contracts synchronously. Thus, as contracts exchange asynchronous messages, it is not possible to directly read the state variables of one contract by another because, by the time a message with the data reaches your contract, the state variables on another contract might have changed already.
248248

249249
Instead, prefer to write contracts that exchange messages as signals to perform a certain action on or with the sent data but never merely to read the state of another contract.
250250

docs/src/content/docs/book/receive.mdx

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ contract Handler() {
8181
}
8282
```
8383

84+
## Wildcard parameters
85+
86+
Naming the parameter of a receiver function with an underscore `_{:tact}` makes its value considered unused and discarded. This is useful when you don't need to inspect the message received and only want it to convey a specific opcode.
87+
88+
```tact
89+
message(42) UniverseCalls {}
90+
91+
contract Example() {
92+
receive(_: UniverseCalls) {
93+
// Got a message with opcode 42
94+
UniverseCalls.opcode(); // 42
95+
}
96+
}
97+
```
98+
99+
Read more about other common function aspects: [Commonalities on the Functions page](/book/functions#commonalities).
100+
84101
## Processing order
85102

86103
All receiver functions are processed in the order they are listed below. The first receiver that matches the message type processes the message:
@@ -123,8 +140,27 @@ Contracts are not required to declare receivers for all possible message types.
123140

124141
Note that the receiver `receive(msg: Slice){:tact}` acts as a fallback that catches all messages that did not match previous receivers in the execution order list. If there is no receiver to process a message type and the fallback receiver `receive(msg: Slice){:tact}` is not declared, the transaction will fail with exit code [130](/book/exit-codes/#130).
125142

143+
## Incoming funds
144+
126145
Receivers accept all incoming funds by default. That is, without explicitly sending a message that will refund spare Toncoin with the [`cashback(){:tact}`](/ref/core-send#cashback) function, the contract will keep all the incoming message value.
127146

147+
```tact
148+
contract ToCashOrNotToCash() {
149+
receive() {
150+
// Forward the remaining value in the
151+
// incoming message back to the sender.
152+
cashback(sender());
153+
}
154+
155+
receive(_: Greed) {
156+
// Unlike the previous one, this receiver does not return surplus Toncoin,
157+
// and keeps all the incoming message value minus necessary fees.
158+
}
159+
}
160+
161+
message Greed {}
162+
```
163+
128164
To check the amount of Toncoin (native coins) the incoming message carries with it, you can use the [`context(){:tact}`](/ref/core-contextstate#context) function and access the `value` field of its resulting [struct](/book/structs-and-messages#structs). Note, however, that the exact value by which the balance will be increased will be less than `context().value{:tact}` since the [compute fee](https://docs.ton.org/v3/documentation/smart-contracts/transaction-fees/fees-low-level#computation-fees) for contract execution will be deducted from this value.
129165

130166
```tact /context\(\).value/
@@ -148,14 +184,55 @@ message(0x706c7567) PluginRequestFunds {
148184
}
149185
```
150186

151-
Naming the parameter of a receiver function with an underscore `_{:tact}` makes its value considered unused and discarded. This is useful when you don't need to inspect the message received and only want it to convey a specific opcode:
187+
## Carry-value pattern
188+
189+
Contracts cannot call each other's [getters](/book/functions#get) or retrieve data from other contracts synchronously. If you were to send the current values of the state variables to another contract, they would almost always become stale by the time the message carrying them is processed at the destination.
190+
191+
To circumvent that, use the carry-value pattern, which involves passing state or data along the chain of required operations rather than attempting to store and retrieve the data in a synchronous fashion. Instead of querying data, each contract in the message chain receives some input, processes it, and passes the result or the new payload to the next contract in the sequence of sent messages. This way, messages work as signals to perform certain actions on or with the sent data.
192+
193+
For example, upon receiving a message request to perform a [Jetton](/cookbook/jettons) transfer from the current owner to the target user's [Jetton Wallet](/cookbook/jettons#jetton-wallet-contract), Jetton Wallet contracts ensure the validity of the request from the standpoint of their current state. If everything is fine, they modify their token balance and always send the deployment message, regardless of whether the target Jetton Wallet exists or not.
194+
195+
That is because it is impossible to obtain confirmation of the contract deployment synchronously. At the same time, sending a deployment message means attaching the [`StateInit{:tact}`](/book/expressions#initof) code and data of the future contract to the regular message and letting the TON Blockchain itself figure out whether the target contract is deployed or not, and discarding that `init` bundle if the destination contract is already deployed.
152196

153197
```tact
154-
message(42) UniverseCalls {}
198+
/// Child contract per each holder of N amount of given Jetton (token)
199+
contract JettonWallet(
200+
/// Balance in Jettons.
201+
balance: Int as coins,
202+
203+
/// Address of the user's wallet which owns this JettonWallet, and messages
204+
/// from whom should be recognized and fully processed.
205+
owner: Address,
206+
207+
/// Address of the main minting contract,
208+
/// which deployed this Jetton wallet for the specific user's wallet.
209+
master: Address,
210+
) {
211+
/// Registers a binary receiver of the JettonTransfer message body.
212+
receive(msg: JettonTransfer) {
213+
// ...prior checks and update of the `self.balance`...
155214
156-
contract Example {
157-
receive(_: UniverseCalls) {
158-
// Got a Message with opcode 42
215+
// Transfers Jetton from the current owner to the target user's JettonWallet.
216+
// If that wallet does not exist, it is deployed on-chain in the same transfer.
217+
deploy(DeployParameters {
218+
value: 0,
219+
mode: SendRemainingValue,
220+
bounce: true,
221+
body: JettonTransferInternal{
222+
queryId: msg.queryId, // Int as uint64
223+
amount: msg.amount, // Int as coins
224+
sender: self.owner, // Address
225+
responseDestination: msg.responseDestination, // Address?
226+
forwardTonAmount: msg.forwardTonAmount, // Int as coins
227+
forwardPayload: msg.forwardPayload, // Slice as remaining
228+
}.toCell(),
229+
// Notice that we do not need to explicitly specify the target address,
230+
// because it will be computed on the fly from the initial package.
231+
//
232+
// The `msg.destination` is the regular wallet address of the new owner
233+
// of those Jettons and not the future address of the target Jetton Wallet itself.
234+
init: initOf JettonWallet(0, msg.destination, self.master),
235+
});
159236
}
160237
}
161238
```

0 commit comments

Comments
 (0)