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

tests: receiver precedence #1742

Merged
merged 17 commits into from
Feb 13, 2025
Merged
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
2 changes: 2 additions & 0 deletions docs/src/content/docs/book/external.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@ contract SampleContract {
}
}
```

External receivers follow the same execution order conventions as [internal receivers](/book/receive).
12 changes: 11 additions & 1 deletion docs/src/content/docs/book/receive.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ TON is a distributed blockchain which means that communication between contracts

To receive a message of the required type, you need to declare a receiver function, for example, `receive("increment"){:tact}`. This notation means the declaration of a receiver function that will be called when a text with the value `"increment"{:tact}` is sent to the contract. The function body can modify the state of the contract and send messages to other contracts. It is impossible to call a receiver directly. If you need to reuse some logic you can declare a function and call it from the receiver.

There are several receiver functions. All receiver functions are processed in the order they are listed below:
There are several receiver functions. All receiver functions are processed in the order they are listed below. The first receiver that matches the message type will process the message:

* `receive(){:tact}` - called when an empty message is sent to the contract
* `receive("message"){:tact}` - called when a text message with a specific comment is sent to the contract (maximum `"message"{:tact}` length is 123 bytes)
* `receive(str: String){:tact}` - called when an arbitrary text message is sent to the contract
* `receive(msg: MyMessage){:tact}` - called when a binary message of type `MyMessage` is sent to the contract
* `receive(msg: Slice){:tact}` - called when binary message of unknown type is sent to the contract

For example, an empty message gets processed by `receive(){:tact}` and not by `receive(msg: Slice){:tact}`, because the former occurs before the later in the above list. Similarly, a message with specific comment `"message"{:tact}` gets processed by `receive("message"){:tact}` and not by `receive(str: String){:tact}`.

```tact
message MyMessage {
value: Int;
Expand All @@ -44,6 +46,14 @@ contract MyContract {
}
```

In a contract, the order of declaration of receivers has no effect on how receivers process messages. Hence, changing the order of receivers in the above contract produces an equivalent contract.

Contracts are not required to declare receivers for all possible message types. If a contract does not have a receiver for a specific message type, the message will be processed by the next receiver that matches the message type in the receiver execution order list. For example, if we remove the receiver `receive("message"){:tact}` in the above contract, then if a message with comment `"message"{:tact}` arrives, it will get processed by `receive(str: String){:tact}`.

Note that receiver `receive(msg: Slice){:tact}` acts as a fallback that catches all messages that did not match the 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).

Naming a parameter of the 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 you only want it to convey a specific opcode:

```tact
Expand Down
153 changes: 153 additions & 0 deletions src/test/e2e-emulated/contracts/receiver-precedence.tact
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
message Message {
msg: String;
}

message BinaryIntOperation {
op: String;
val1: Int;
val2: Int;
}

message BinaryIntResult {
val: Int;
}

// This contract receives binary arithmetic requests.
// It only supports divisions.
contract Calculator {

receive("deploy") {}

receive(request: BinaryIntOperation) {
require(request.op == "/", "Only divisions are currently supported.");
let result = request.val1 / request.val2;
send(SendParameters{
to: sender(),
bounce: false,
value: ton("1"),
body: BinaryIntResult{val: result}.toCell(),
});
}
}

contract ReceiverTester {

receiverKind: String = "unknown";

receive() {
self.receiverKind = "empty";
}

receive(msg: String) {
if (msg == "message") {
self.receiverKind = "error_comment";
} else {
self.receiverKind = "comment_fallback";
}
}

receive(msg: Message) {
self.receiverKind = "binary_message";
}

receive(msg: Slice) {
// Drop the op code
msg.loadUint(32);
let m = msg.asString();
if (m == "message") {
self.receiverKind = "message_slice";
} else {
self.receiverKind = "fallback";
}
}

receive("message") {
self.receiverKind = "comment";
}

// Bounced testing

// Initiate a request to the calculator, with an unsupported arithmetic operation: 1 + 1
receive("do_unsupported_op") {
let addr = contractAddress(initOf Calculator());
send(SendParameters{
to: addr,
bounce: true,
value: ton("1"),
body: BinaryIntOperation{op: "+", val1: 1, val2: 1}.toCell(),
});
}

// Initiate a request to the calculator, with a division by zero: 10/0
receive("do_div_by_zero") {
let addr = contractAddress(initOf Calculator());
send(SendParameters{
to: addr,
bounce: true,
value: ton("1"),
body: BinaryIntOperation{op: "/", val1: 10, val2: 0}.toCell(),
});
}

// Initiate a request to the calculator: 10/2
receive("do_success_div") {
let addr = contractAddress(initOf Calculator());
send(SendParameters{
to: addr,
bounce: true,
value: ton("1"),
body: BinaryIntOperation{op: "/", val1: 10, val2: 2}.toCell(),
});
}

// Initiate a non-arithmetic request to the calculator.
// The calculator will reject the request
receive("do_unknown_request") {
let addr = contractAddress(initOf Calculator());
send(SendParameters{
to: addr,
bounce: true,
value: ton("1"),
body: "do_something".asComment(),
});
}

bounced(msg: Slice) {
self.receiverKind = "bounced_fallback";
}

bounced(msg: bounced<BinaryIntOperation>) {
self.receiverKind = "bounced_binary_message";
}

// External receiver testing

external(msg: String) {
acceptMessage();
if (msg == "message") {
self.receiverKind = "external_error_comment";
} else {
self.receiverKind = "external_comment_fallback";
}
}

external() {
acceptMessage();
self.receiverKind = "external_empty";
}

external("message") {
acceptMessage();
self.receiverKind = "external_comment";
}

external(msg: Message) {
acceptMessage();
self.receiverKind = "external_binary_message";
}

get fun receiverKind(): String {
return self.receiverKind;
}
}

Loading
Loading