diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index 549981d30..ad92fbc10 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -87,6 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support overriding constants and methods of BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591) - Forbid traits inherit implicitly from BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591) - Forbid the `override` modifier for constants without the corresponding super-constant: PR [#1591](https://github.com/tact-lang/tact/pull/1591) +- Check map types for `deepEquals` method: PR [#1718](https://github.com/tact-lang/tact/pull/1718) - Remove "remainder" from error messages: PR [#1699](https://github.com/tact-lang/tact/pull/1699) - Check map types for `deepEquals` method: PR [#1718](https://github.com/tact-lang/tact/pull/1718) @@ -126,6 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added description of `.boc`, `.ts`, `.abi`, `.pkg` files and completed Compilation page: PR [#1676](https://github.com/tact-lang/tact/pull/1676) - Marked gas-expensive functions and expressions: PR [#1703](https://github.com/tact-lang/tact/pull/1703) - Added a Security audits page, with the first assessment from the Trail of Bits: PR [#1791](https://github.com/tact-lang/tact/pull/1791) +- 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 diff --git a/docs/src/content/docs/book/assembly-functions.mdx b/docs/src/content/docs/book/assembly-functions.mdx index c9b281226..b3c748fc9 100644 --- a/docs/src/content/docs/book/assembly-functions.mdx +++ b/docs/src/content/docs/book/assembly-functions.mdx @@ -478,7 +478,7 @@ When there are literals involved, they'll be shown as is. Additionally, when val ```tact // Computes and returns the Keccak-256 hash as an 256-bit unsigned `Int` -// from a passed `Slice` `s`. Uses the Ethereum-compatible implementation. +// from a passed `Slice` `s`. Uses the Ethereum-compatible* implementation. asm fun keccak256(s: Slice): Int { // s:Slice → s:Slice, 1 // ————————————————————— @@ -498,8 +498,8 @@ The [`HASHEXT_KECCAK512`](https://docs.ton.org/v3/documentation/tvm/instructions ```tact // Computes and returns the Keccak-512 hash in two 256-bit unsigned `Int` -// values from a passed `Slice` `s`. Uses the Ethereum-compatible implementation. -asm fun keccak256(s: Slice): Hash512 { +// values from a passed `Slice` `s`. Uses the Ethereum-compatible* implementation. +asm fun keccak512(s: Slice): Hash512 { // s:Slice → s:Slice, 1 // ————————————————————— // s0 → s1 s0 @@ -520,6 +520,12 @@ asm fun keccak256(s: Slice): Hash512 { struct Hash512 { h1: Int; h2: Int } ``` +While it is said that these sample `keccak256(){:tact}` and `keccak512(){:tact}` functions use the Ethereum-compatible implementation, note that the underlying `HASHEXT` family of [TVM][tvm] instructions has its own drawbacks. + +These drawbacks stem from the limitations of the [`Slice{:tact}`][slice] type itself — `HASHEXT_KECCAK256` and other hashing instructions of the `HASHEXT` family ignore any references present in the passed slice(s), i.e. only up to $1023$ bits of its data are used. + +To work around this, you can recursively load all the refs from the given [`Slice{:tact}`][slice], and then hash them all at once by specifying their exact number instead of the `ONE` [TVM][tvm] instruction used earlier. See an example below: [`onchainSha256`](#onchainsha256). + :::note[Useful links:] [`HASHEXT_KECCAK256`](https://docs.ton.org/v3/documentation/tvm/instructions#F90403)\ diff --git a/docs/src/content/docs/book/maps.mdx b/docs/src/content/docs/book/maps.mdx index c47d52471..84436b575 100644 --- a/docs/src/content/docs/book/maps.mdx +++ b/docs/src/content/docs/book/maps.mdx @@ -91,14 +91,19 @@ No other [allowed key or value types](#allowed-types) besides [`Int{:tact}`][int ### Declare, `emptyMap()` {#emptymap} -As a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library: +```tact +// K and V correspond to the key and value types of the target map +fun emptyMap(): map; +``` + +Declaring a map as a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library: ```tact let fizz: map = emptyMap(); let fizz: map = null; // identical to the previous line, but less descriptive ``` -As a [persistent state variable](/book/contracts#variables): +Declaring a map as a [persistent state variable](/book/contracts#variables): ```tact contract Example { @@ -412,7 +417,7 @@ There, both maps are formed manually and both contain the same key-value pair. I ```tact // K and V correspond to the key and value types of the given map -extends fun asCell(self: map): Cell; +extends fun asCell(self: map): Cell?; ``` On [TVM][tvm], maps are represented as a [`Cell{:tact}`][cell] type and it's possible to construct and parse them directly. However, doing so is highly error-prone and quite messy, which is why Tact provides maps as a standalone composite type with many of the helper methods mentioned above. diff --git a/docs/src/content/docs/book/message-mode.mdx b/docs/src/content/docs/book/message-mode.mdx index 53e7ce634..025855979 100644 --- a/docs/src/content/docs/book/message-mode.mdx +++ b/docs/src/content/docs/book/message-mode.mdx @@ -9,7 +9,7 @@ As it was previously mentioned, messages sent via [`send(){:tact}`](/ref/core-co It's possible to use raw [`Int{:tact}`][int] values and manually provide them for the `mode`, but for your convenience there's a set of constants which you may use to construct the compound `mode` with ease. Take a look at the following tables for more information on base modes and optional flags. -Note that there are other [message sending functions](/book/send#message-sending-functions) — they do not use the `SendParameters{:tact}` [Struct](/book/structs-and-messages#structs), but accept the `mode` as one of their parameters. +Note that there are other [message-sending functions](/book/send#message-sending-functions) — they do not use the `SendParameters{:tact}` [Struct](/book/structs-and-messages#structs), but accept the `mode` as one of their parameters. ## Base modes @@ -17,8 +17,14 @@ Mode value | Constant name | Description ---------: | :---------------------------- | ----------- $0$ | `SendDefaultMode{:tact}` | Ordinary message (default). $64$ | `SendRemainingValue{:tact}` | Carry all the remaining value of the inbound message in addition to the value initially indicated in the new message. -$128$ | `SendRemainingBalance{:tact}` | Carry all the remaining balance of the current smart contract instead of the value originally indicated in the message. -$1024$ | `SendOnlyEstimateFee{:tact}` | Doesn't send the message, only estimates the forward fees if the [message sending function](/book/send#message-sending-functions) computes those. +$128$ | `SendRemainingBalance{:tact}` | Carry **all the remaining balance** of the current smart contract instead of the value originally indicated in the message. +$1024$ | `SendOnlyEstimateFee{:tact}` | Doesn't send the message, only estimates the forward fees if the [message-sending function](/book/send#message-sending-functions) computes those. + +The base mode `SendRemainingValue{:tact}` does **not** take previous actions into account, i.e. it doesn't recalculate the remaining value of the incoming message based on previously sent messages or performed actions during [action phase](https://docs.ton.org/learn/tvm-instructions/tvm-overview#transactions-and-phases). + +Unlike `SendRemainingValue{:tact}`, the base mode `SendRemainingBalance{:tact}` always calculates the current value of the contract balance, which can help solve problems with [complex outbound message processing](/book/send#outbound-message-processing). + +However, be **very** careful when using `SendRemainingBalance{:tact}`, because it works with the balance of the whole contract and any mistake with it can lead to a total loss of funds. ## Optional flags @@ -50,16 +56,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 diff --git a/docs/src/content/docs/book/send.mdx b/docs/src/content/docs/book/send.mdx index d5e48884e..06aad859b 100644 --- a/docs/src/content/docs/book/send.mdx +++ b/docs/src/content/docs/book/send.mdx @@ -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: @@ -101,9 +101,11 @@ send(SendParameters{ ## Outbound message processing -Each transaction on TON Blockchain consists of [multiple phases][phases]. Outbound messages are evaluated in [compute phase][compute], but are **not** sent in that phase. Instead, they're queued in order of appearance for the [action phase][phases], where all actions listed in [compute phase][compute], like outbound messages or [reserve requests](/ref/core-advanced#nativereserve), are executed. +Each transaction on TON Blockchain consists of [multiple phases][phases]. Outbound messages are evaluated in [compute phase][compute], but are **not** sent in that phase. Instead, they're queued for execution in the [action phase][phases] in order of their appearance in the compute phase. -As all the values are computed in [compute phase][compute], all the fees computed by the end of it, and exceptions do not revert the transaction during [action phase][phases], outbound message sends can fail without bounce due to insufficient [action fees](https://docs.ton.org/develop/howto/fees-low-level#action-fee) or [forward fees][fwdfee]. +Outgoing message sends may fail in [action phase][phases] due to insufficient [action fees](https://docs.ton.org/develop/howto/fees-low-level#action-fee) or [forward fees][fwdfee], 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][compute], 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][phases] in case they cannot be sent, set the optional [`SendIgnoreErrors{:tact}`](/book/message-mode#optional-flags) flag when composing the message. Consider the following example: @@ -112,20 +114,23 @@ 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! } ``` @@ -133,9 +138,94 @@ 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][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). -* 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. + Alternatively, see the suggested solutions below. + +::: + +Previous examples discussed a case where the contract has $0$ [nanoToncoins][nano] 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{:tact}`](/book/message-mode#base-modes) base mode, paired with the _necessary_ call to the [`nativeReserve(){:tact}`](/ref/core-advanced#nativereserve) function. + +Like outbound messages, [reserve requests](/ref/core-advanced#nativereserve) are queued during the [compute phase][compute] and executed during the [action phase][phases]. + +```tact +// This contract has some Toncoins on the balance, say, 0.2 or more +contract 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{:tact}`](/ref/core-base#self-storagereserve) constant: + +* [`self.reply(){:tact}`](/ref/core-base#self-reply) +* [`self.notify(){:tact}`](/ref/core-base#self-notify) +* [`self.forward(){:tact}`](/ref/core-base#self-forward) + +If you take only one thing away from this section, please remember this: be very careful with the [base modes](/book/message-mode#base-modes) of the message-sending functions, including the [implicitly set modes](/book/message-mode#functions-with-implicit-mode). ## Message sending limits @@ -143,9 +233,9 @@ In total, there could be no more than $255$ actions queued for execution, which Attempts to queue more throw an exception with an [exit code 33](/book/exit-codes#33) during [action phase][phases]: `Action list is too long`. -## Message sending functions +## Message-sending functions -Read more about all message sending functions in the Reference: +Read more about all message-sending functions in the Reference: * [`send(){:tact}`](/ref/core-common#send) * [`emit(){:tact}`](/ref/core-common#emit) @@ -159,6 +249,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 diff --git a/docs/src/content/docs/ref/core-advanced.mdx b/docs/src/content/docs/ref/core-advanced.mdx index 8c4c7b42c..6f0c682e2 100644 --- a/docs/src/content/docs/ref/core-advanced.mdx +++ b/docs/src/content/docs/ref/core-advanced.mdx @@ -610,6 +610,8 @@ Attempts to queue more than $255$ messages throw an exception with an [exit code ## nativeReserve +

+ ```tact fun nativeReserve(amount: Int, mode: Int); ``` diff --git a/docs/src/content/docs/ref/core-base.mdx b/docs/src/content/docs/ref/core-base.mdx index e23150d18..49809ba01 100644 --- a/docs/src/content/docs/ref/core-base.mdx +++ b/docs/src/content/docs/ref/core-base.mdx @@ -91,14 +91,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. ::: diff --git a/docs/src/content/docs/ref/core-common.mdx b/docs/src/content/docs/ref/core-common.mdx index dc1ad2c25..18ae99a0e 100644 --- a/docs/src/content/docs/ref/core-common.mdx +++ b/docs/src/content/docs/ref/core-common.mdx @@ -39,7 +39,7 @@ let iNeedADolla: Int = myBalance(); :::caution - Beware, that [all message sending functions](/book/send#message-sending-functions) of Tact can change the _actual_ contract's balance, but they _won't_ update the value returned by this function. + Beware, that [all message-sending functions](/book/send#message-sending-functions) of Tact can change the _actual_ contract's balance, but they _won't_ update the value returned by this function. ::: @@ -97,10 +97,10 @@ Returns `Context{:tact}` [Struct](/book/structs-and-messages#structs), that cons Field | Type | Description :-------- | :-------------------- | :---------- -`bounced` | [`Bool{:tact}`][bool] | [Bounced](https://ton.org/docs/learn/overviews/addresses#bounceable-vs-non-bounceable-addresses) flag of the incoming message. +`bounced` | [`Bool{:tact}`][bool] | [Bounced](https://ton.org/docs/learn/overviews/addresses#bounceable-vs-non-bounceable-addresses) flag of the received message. `sender` | [`Address{:tact}`][p] | Internal address of the sender on the TON blockchain. -`value` | [`Int{:tact}`][int] | Amount of [nanoToncoins](/book/integers#nanotoncoin) in a message. -`raw` | [`Slice{:tact}`][slice] | The remainder of the message as a [`Slice{:tact}`][slice]. It follows [internal message layout](https://docs.ton.org/develop/smart-contracts/messages#message-layout) of TON starting from the destination [`Address{:tact}`][p] (`MsgAddressInt` in [TL-B notation](https://docs.ton.org/develop/data-formats/tl-b-language)). +`value` | [`Int{:tact}`][int] | Amount of [nanoToncoins](/book/integers#nanotoncoin) in the received message. +`raw` | [`Slice{:tact}`][slice] | The remainder of the received message as a [`Slice{:tact}`][slice]. It follows [internal message layout](https://docs.ton.org/develop/smart-contracts/messages#message-layout) of TON starting from the destination [`Address{:tact}`][p] (`MsgAddressInt` in [TL-B notation](https://docs.ton.org/develop/data-formats/tl-b-language)). Usage example: @@ -233,7 +233,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`. diff --git a/src/test/e2e-emulated/contracts/send.tact b/src/test/e2e-emulated/contracts/send.tact index 2bc347f97..c942b7737 100644 --- a/src/test/e2e-emulated/contracts/send.tact +++ b/src/test/e2e-emulated/contracts/send.tact @@ -10,4 +10,44 @@ contract SendTester with Deployable { self.reply("World".asComment()); emit("Something".asComment()); } -} \ No newline at end of file + + /// A no-op receiver for incoming funds + receive("topup") {} + + /// Sends a message via emit(), + /// keeps the original balance, + /// then sends the rest in the next message via send() + receive("ReserveAtMost_1") { + emit("Have you seen this message?".asComment()); + + nativeReserve(ton("0.05"), ReserveAtMost | ReserveAddOriginalBalance); + + send(SendParameters{ + to: sender(), + value: 0, + mode: SendRemainingBalance, + body: "I give you my all! Well, all that's not mine!".asComment(), + }); + } + + /// Sends a message to our "topup" receiver via send(), + /// keeps the original balance, + /// then sends the rest in the next message via send() + receive("ReserveAtMost_2") { + send(SendParameters{ + to: myAddress(), + value: 0, + mode: SendDefaultMode | SendPayGasSeparately, + body: "topup".asComment(), + }); + + nativeReserve(ton("0.05"), ReserveAtMost | ReserveAddOriginalBalance); + + send(SendParameters{ + to: sender(), + value: 0, + mode: SendRemainingBalance, + body: "I give almost all that's not mine!".asComment(), + }); + } +} diff --git a/src/test/e2e-emulated/send.spec.ts b/src/test/e2e-emulated/send.spec.ts index f2d02e3f7..49900f392 100644 --- a/src/test/e2e-emulated/send.spec.ts +++ b/src/test/e2e-emulated/send.spec.ts @@ -1,4 +1,4 @@ -import { toNano, beginCell } from "@ton/core"; +import { toNano, beginCell, Cell } from "@ton/core"; import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { SendTester } from "./contracts/output/send_SendTester"; import "@ton/test-utils"; @@ -61,4 +61,87 @@ describe("send", () => { exitCode: 130, }); }); + + it("should send with intermediate reservations", async () => { + // emit, nativeReserve, send + let balanceBefore = (await blockchain.getContract(contract.address)) + .balance; + await expectMessageFromToWithDefaults({ + treasure, + contract, + body: textMsg("ReserveAtMost_1"), + }); + let balanceAfter = (await blockchain.getContract(contract.address)) + .balance; + // The difference is at most 0.05 Toncoin reserved on top of the previous balance + expect(balanceAfter - balanceBefore <= toNano("0.05")).toBe(true); + + // send, nativeReserve, send + balanceBefore = (await blockchain.getContract(contract.address)) + .balance; + await expectMessageFromToWithDefaults({ + treasure, + contract, + body: textMsg("ReserveAtMost_2"), + }); + balanceAfter = (await blockchain.getContract(contract.address)).balance; + // The difference is at most 0.05 Toncoin reserved on top of the previous balance + expect(balanceAfter - balanceBefore <= toNano("0.05")).toBe(true); + }); }); + +/** + * A helper function to send a message `body` from the `treasury` to the `contract` + * with specified `value` and `bounce` values, and then expect that transaction + * to be successful or not (`success`), and if not — expect a certain exit code from it + */ +async function expectMessageFromTo(args: { + treasure: SandboxContract; + contract: SandboxContract; + body: Cell | null; + value: bigint; + bounce: boolean; + success: boolean; + exitCode: number; +}) { + const sendResult = await args.treasure.send({ + to: args.contract.address, + value: args.value, + bounce: args.bounce, + body: args.body, + }); + expect(sendResult.transactions).toHaveTransaction({ + from: args.treasure.address, + to: args.contract.address, + success: args.success, + exitCode: args.exitCode, + }); +} + +/** + * Like `expectMessageFromTo`, but with common defaults set: + * * value: `toNano("10")` + * * bounce: `false` + * * success: `true` + * * exitCode: `0` + */ +async function expectMessageFromToWithDefaults(args: { + treasure: SandboxContract; + contract: SandboxContract; + body: Cell | null; +}) { + await expectMessageFromTo({ + treasure: args.treasure, + contract: args.contract, + body: args.body, + value: toNano("10"), + bounce: false, + success: true, + exitCode: 0, + }); +} + +/** Creates a Cell message body from the passed `src` string */ +function textMsg(src: string): Cell { + return beginCell().storeUint(0, 32).storeStringTail(src).endCell(); +}