Skip to content
Open
5 changes: 5 additions & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ The documentation changelog is kept separately: [CHANGELOG-DOCS](./CHANGELOG-DOC

- Reordered arguments of the `__tact_store_address_opt` function to optimize gas consumption: PR [#3333](https://github.com/tact-lang/tact/pull/3333)

### Language features

- Support `.send()` method from all message-like structures

## [1.6.13] - 2025-05-29

### Language features
Expand All @@ -23,6 +27,7 @@ The documentation changelog is kept separately: [CHANGELOG-DOCS](./CHANGELOG-DOC

- [Anton Trunov](https://github.com/anton-trunov)
- [Novus Nota](https://github.com/novusnota)
- [skywardboundd](https://github.com/skywardboundd)

## [1.6.12] - 2025-05-27

Expand Down
3 changes: 3 additions & 0 deletions docs/src/content/docs/book/send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ Read more about all message-sending functions in the Reference:
* [`send(){:tact}`](/ref/core-send#send)
* [`message(){:tact}`](/ref/core-send#message)
* [`deploy(){:tact}`](/ref/core-send#deploy)
* [`SendParameters.send(){:tact}`](/ref/core-send#sendparameters-send)
* [`MessageParameters.send(){:tact}`](/ref/core-send#messageparameters-send)
* [`DeployParameters.send(){:tact}`](/ref/core-send#deployparameters-send)
* [`emit(){:tact}`](/ref/core-send#emit)
* [`cashback(){:tact}`](/ref/core-send#cashback)
* [`self.notify(){:tact}`](/ref/core-base#self-notify)
Expand Down
107 changes: 107 additions & 0 deletions docs/src/content/docs/ref/core-send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,38 @@ send(SendParameters {

:::

### SendParameters.send {#sendparameters-send}

<Badge text="Gas-expensive" title="Uses 500 gas units or more" variant="danger" size="medium"/>
<Badge text="Available since Tact 1.7" variant="tip" size="medium"/><p/>

```tact
extends fun send(self: SendParameters);
```

[Queues the message](/book/send#outbound-message-processing) to be sent using a [`SendParameters{:tact}`](/book/send) [struct][struct].

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

Usage example:

```tact
SendParameters {
to: sender(), // back to the sender,
value: ton("1"), // with 1 Toncoin (1_000_000_000 nanoToncoin),
// and no message body
}.send();
```

:::note[Useful links:]

[Sending messages in the Book](/book/send)\
[Message `mode` in the Book](/book/message-mode)\
[Single-contract communication in the Cookbook](/cookbook/single-communication)\
[`nativeReserve(){:tact}`](/ref/core-contextstate#nativereserve)

:::

### message

<Badge text="500+ gas" title="Uses 500 gas units or more" variant="danger" size="medium"/>
Expand Down Expand Up @@ -75,6 +107,39 @@ message(MessageParameters {

:::

### MessageParameters.send {#messageparameters-send}

<Badge text="Gas-expensive" title="Uses 500 gas units or more" variant="danger" size="medium"/>
<Badge text="Available since Tact 1.7" variant="tip" size="medium"/><p/>

```tact
extends fun send(self: MessageParameters);
```

[Queues the message](/book/send#outbound-message-processing) to be sent using the `MessageParameters{:tact}` [struct][struct]. Allows for cheaper non-deployment regular messages compared to the [`send(){:tact}`](#send) function.

The `MessageParameters{:tact}` [struct][struct] is similar to the [`SendParameters{:tact}`](/book/send) [struct][struct], but without the `code` and `data` fields.

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

Usage example:

```tact
MessageParameters {
to: sender(), // back to the sender,
value: ton("1"), // with 1 Toncoin (1_000_000_000 nanoToncoin),
// and no message body
}.send();
```

:::note[Useful links:]

[Sending messages in the Book](/book/send)\
[Message `mode` in the Book](/book/message-mode)\
[`nativeReserve(){:tact}`](/ref/core-contextstate#nativereserve)

:::

### deploy

<Badge text="500+ gas" title="Uses 500 gas units or more" variant="danger" size="medium"/>
Expand Down Expand Up @@ -117,6 +182,48 @@ deploy(DeployParameters {

:::

### DeployParameters.send {#deployparameters-send}

<Badge text="Gas-expensive" title="Uses 500 gas units or more" variant="danger" size="medium"/>
<Badge text="Available since Tact 1.7" variant="tip" size="medium"/><p/>

```tact
extends fun send(self: DeployParameters);
```

[Queues](/book/send#outbound-message-processing) the [contract deployment message](/book/deploy) to be sent using the `DeployParameters{:tact}` [struct][struct]. Allows for cheaper on-chain deployments compared to the [`send(){:tact}`](#send) function.

The `DeployParameters{:tact}` [struct][struct] consists of the following fields:

Field | Type | Description
:------- | :---------------------------- | :----------
`mode` | [`Int{:tact}`][int] | An 8-bit value that configures how to send a message, defaults to $0$. See: [Message `mode`](/book/message-mode).
`body` | [`Cell?{:tact}`][cell] | [Optional][opt] message body as a [`Cell{:tact}`][cell].
`value` | [`Int{:tact}`][int] | The amount of [nanoToncoins][nano] you want to send with the message. This value is used to cover [forward fees][fwdfee] unless the optional flag [`SendPayFwdFeesSeparately{:tact}`](/book/message-mode#optional-flags) is used.
`bounce` | [`Bool{:tact}`][p] | When set to `true` (default), the message bounces back to the sender if the recipient contract doesn't exist or isn't able to process the message.
`init` | [`StateInit{:tact}`][initpkg] | [Initial package][initpkg] of the contract (initial code and initial data). See: [`initOf{:tact}`][initpkg].

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

Usage example:

```tact
DeployParameters {
init: initOf SomeContract(), // with initial code and data of SomeContract
// and no additional message body
mode: SendIgnoreErrors, // skip the message in case of errors
value: ton("1"), // send 1 Toncoin (1_000_000_000 nanoToncoin)
}.send();
```

:::note[Useful links:]

[Sending messages in the Book](/book/send)\
[Message `mode` in the Book](/book/message-mode)\
[`nativeReserve(){:tact}`](/ref/core-contextstate#nativereserve)

:::

### cashback

<Badge text="500+ gas" title="Uses 500 gas units or more" variant="danger" size="medium"/>
Expand Down
20 changes: 10 additions & 10 deletions src/benchmarks/escrow/tact/escrow.tact
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract Escrow(
self.assetAddress!!,
self.jettonWalletCode!!,
);
message(MessageParameters {
MessageParameters {
to: escrowJettonWalletAddress,
value: self.JETTON_TRANSFER_GAS,
body: JettonTransfer {
Expand All @@ -69,7 +69,7 @@ contract Escrow(
customPayload: null,
}.toCell(),
mode,
});
}.send();
}

receive(_: Funding) {
Expand Down Expand Up @@ -122,16 +122,16 @@ contract Escrow(

if (self.assetAddress == null) {
throwUnless(404, ctx.value > (2 * self.TON_TRANSFER_GAS));
message(MessageParameters {
MessageParameters {
to: self.sellerAddress,
value: self.dealAmount - royaltyAmount,
mode: SendPayFwdFeesSeparately,
});
message(MessageParameters {
}.send();
MessageParameters {
to: self.guarantorAddress,
value: royaltyAmount,
mode: SendRemainingBalance | SendDestroyIfZero, // send all remaining and destroy escrow
});
}.send();
} else {
throwUnless(404, ctx.value > (2 * self.JETTON_TRANSFER_GAS));
self.sendJettons(self.sellerAddress, self.dealAmount - royaltyAmount, SendPayFwdFeesSeparately);
Expand All @@ -147,19 +147,19 @@ contract Escrow(
// it's up to business logic to decide if guarantor gets royalty in case of cancel
// in this implementation we don't pay royalty in case of cancel
if (self.assetAddress == null) {
message(MessageParameters {
MessageParameters {
to: self.buyerAddress!!,
value: self.dealAmount,
mode: SendRemainingBalance | SendDestroyIfZero, // send all remaining and destroy escrow
});
}.send();
} else {
self.sendJettons(self.buyerAddress!!, self.dealAmount, SendRemainingBalance | SendDestroyIfZero);
}
}

// on-chain data access, could be useful to guarantor contract
receive(_: ProvideEscrowData) {
message(MessageParameters {
MessageParameters {
bounce: false,
value: 0,
to: sender(),
Expand All @@ -177,7 +177,7 @@ contract Escrow(
},
}.toCell(),
mode: SendRemainingValue,
});
}.send();
}

get fun calculateRoyaltyAmount(): Int {
Expand Down
12 changes: 6 additions & 6 deletions src/benchmarks/jetton/tact/minter.tact
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ contract JettonMinter(
self.totalSupply -= msg.amount;

if (msg.responseDestination.isNotNone()) {
message(MessageParameters {
MessageParameters {
to: msg.responseDestination,
body: JettonExcesses { queryId: msg.queryId }.toCell(),
value: 0,
bounce: false,
mode: SendRemainingValue | SendIgnoreErrors, // ignore errors, because supply has already been updated
});
}.send();
}
}

Expand All @@ -50,12 +50,12 @@ contract JettonMinter(
? contractBasechainAddress(initOf JettonWallet(msg.ownerAddress, myAddress(), 0))
: emptyBasechainAddress();

message(MessageParameters {
MessageParameters {
body: makeTakeWalletAddressMsg(targetJettonWallet, msg),
to: sender(),
value: 0,
mode: SendRemainingValue,
});
}.send();
}

receive(msg: JettonUpdateContent) {
Expand All @@ -67,13 +67,13 @@ contract JettonMinter(
throwUnless(73, sender() == self.owner);
self.totalSupply += msg.mintMessage.amount;

deploy(DeployParameters {
DeployParameters {
value: 0,
bounce: true,
mode: SendRemainingValue,
body: msg.mintMessage.toCell(),
init: getJettonWalletInit(msg.receiver),
});
}.send();
}

receive(msg: ChangeOwner) {
Expand Down
16 changes: 8 additions & 8 deletions src/benchmarks/jetton/tact/wallet.tact
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ contract JettonWallet(
fwdCount * ctx.readForwardFee() +
(2 * self.gasConsumption + self.minTonsForStorage));

deploy(DeployParameters {
DeployParameters {
value: 0,
mode: SendRemainingValue,
bounce: true,
Expand All @@ -40,7 +40,7 @@ contract JettonWallet(
forwardPayload: msg.forwardPayload,
}.toCell(),
init: initOf JettonWallet(msg.destination, self.master, 0),
});
}.send();
}

receive(msg: JettonTransferInternal) {
Expand All @@ -61,7 +61,7 @@ contract JettonWallet(
if (msg.forwardTonAmount > 0) {
let fwdFee: Int = ctx.readForwardFee();
msgValue -= msg.forwardTonAmount + fwdFee;
message(MessageParameters {
MessageParameters {
to: self.owner,
value: msg.forwardTonAmount,
mode: SendPayFwdFeesSeparately,
Expand All @@ -72,18 +72,18 @@ contract JettonWallet(
sender: msg.sender,
forwardPayload: msg.forwardPayload,
}.toCell(),
});
}.send();
}

// 0xd53276db -- Cashback to the original Sender
if (msg.responseDestination != null && msgValue > 0) {
message(MessageParameters {
MessageParameters {
to: msg.responseDestination!!,
value: msgValue,
mode: SendIgnoreErrors,
bounce: false,
body: JettonExcesses { queryId: msg.queryId }.toCell(),
});
}.send();
}
}

Expand All @@ -97,7 +97,7 @@ contract JettonWallet(
let fwdFee: Int = ctx.readForwardFee();
throwUnless(707, ctx.value > (fwdFee + 2 * self.gasConsumption));

message(MessageParameters {
MessageParameters {
to: self.master,
value: 0,
mode: SendRemainingValue,
Expand All @@ -108,7 +108,7 @@ contract JettonWallet(
sender: self.owner,
responseDestination: msg.responseDestination,
}.toCell(),
});
}.send();
}

receive(_: Slice) { throw(0xffff) }
Expand Down
8 changes: 4 additions & 4 deletions src/benchmarks/nft/tact/collection.tact
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ contract NFTCollection {
}

receive(msg: GetRoyaltyParams) {
message(MessageParameters {
MessageParameters {
bounce: false,
to: sender(),
value: 0,
Expand All @@ -46,7 +46,7 @@ contract NFTCollection {
params: self.royaltyParams,
}.toCell(),
mode: SendRemainingValue,
});
}.send();
}

receive(msg: BatchDeploy) {
Expand Down Expand Up @@ -112,12 +112,12 @@ contract NFTCollection {
inline fun deployNFTItem(commonCode: Cell, itemIndex: Int, amount: Int, initNFTBody: Cell, commonData: Builder) {
let data: Cell = commonData.storeUint(itemIndex, 64).endCell(); // IndexSizeBits = 64

deploy(DeployParameters {
DeployParameters {
value: amount,
body: initNFTBody,
init: StateInit { code: commonCode, data },
mode: SendPayFwdFeesSeparately,
});
}.send();
}

struct DictGet {
Expand Down
9 changes: 9 additions & 0 deletions src/benchmarks/notcoin/gas.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@
"burn": "12027",
"discovery": "5818"
}
},
{
"label": "1.6.13 with new send",
"pr": "https://github.com/tact-lang/tact/pull/3056",
"gas": {
"transfer": "15635",
"burn": "12027",
"discovery": "5698"
}
}
]
}
Loading