diff --git a/jest-fast.config.js b/jest-fast.config.js index ad3fdbffa..dbbb58d01 100644 --- a/jest-fast.config.js +++ b/jest-fast.config.js @@ -11,7 +11,6 @@ module.exports = { "/src/test/e2e-emulated/map*", "/src/cli/e2e.spec.ts", "/src/ast/fuzz.spec.ts", - "/src/test/benchmarks/jetton/jetton.spec.ts", ], maxWorkers: "8", }; diff --git a/src/generator/writers/writeContract.ts b/src/generator/writers/writeContract.ts index c0579e200..dea1d65a7 100644 --- a/src/generator/writers/writeContract.ts +++ b/src/generator/writers/writeContract.ts @@ -23,6 +23,7 @@ import { import type { ItemOrigin } from "../../imports/source"; import { resolveFuncTypeFromAbiUnpack } from "./resolveFuncTypeFromAbiUnpack"; import { getAllocation } from "../../storage/resolveAllocation"; +import { contractErrors } from "../../abi/errors"; const SMALL_CONTRACT_MAX_FIELDS = 5; @@ -393,6 +394,14 @@ export function writeMainContract( wCtx.append(); } + wCtx.append(";; message opcode reader utility"); + wCtx.append( + `;; Returns 32 bit message opcode, otherwise throws the "Invalid incoming message" exit code`, + ); + wCtx.append( + `(slice, int) ~load_opcode(slice s) asm( -> 1 0) "32 LDUQ ${contractErrors.invalidMessage.id} THROWIFNOT";`, + ); + wCtx.append(`;;`); wCtx.append(`;; Routing of a Contract ${contract.name}`); wCtx.append(`;;`); diff --git a/src/generator/writers/writeRouter.ts b/src/generator/writers/writeRouter.ts index e4fd84d25..510bde688 100644 --- a/src/generator/writers/writeRouter.ts +++ b/src/generator/writers/writeRouter.ts @@ -71,7 +71,8 @@ export function writeNonBouncedRouter( typeof receivers.fallback !== "undefined" && receivers.binary.length === 0 && receivers.comment.length === 0 && - typeof receivers.commentFallback === "undefined" + typeof receivers.commentFallback === "undefined" && + typeof receivers.empty === "undefined" ) { writeFallbackReceiver(receivers.fallback, contract, "in_msg", wCtx); return; @@ -91,7 +92,7 @@ export function writeNonBouncedRouter( typeof receivers.commentFallback === "undefined" && typeof receivers.fallback === "undefined" ) { - wCtx.append(`var (op, _) = in_msg~load_uint_quiet(32);`); + wCtx.append("var op = in_msg~load_opcode();"); writeBinaryReceivers(true); @@ -216,14 +217,17 @@ function writeCommentReceivers( ) => { const writeFallbackTextReceiverInternal = () => { wCtx.append(";; Fallback Text Receiver"); - const inMsg = msgOpcodeRemoved ? "in_msg" : "in_msg.skip_bits(32)"; - writeFallbackReceiver( - commentFallbackReceiver, - contract, - inMsg, - wCtx, - ); - wCtx.append("return ();"); + wCtx.inBlock("if (in_msg_length >= 32)", () => { + const inMsg = msgOpcodeRemoved + ? "in_msg" + : "in_msg.skip_bits(32)"; + writeFallbackReceiver( + commentFallbackReceiver, + contract, + inMsg, + wCtx, + ); + }); }; // We optimize fallback diff --git a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap index c5725b4a8..ac30a07b4 100644 --- a/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap +++ b/src/test/benchmarks/__snapshots__/benchmarks.spec.ts.snap @@ -14,17 +14,17 @@ exports[`benchmarks benchmark codeOf vs myCode(): gas used myCode 1`] = `931n`; exports[`benchmarks benchmark contractAddressExt: gas used contractAddressExt 1`] = `2589n`; -exports[`benchmarks benchmark deployable trait vs raw deploy: gas used deploy trait 1`] = `4576`; +exports[`benchmarks benchmark deployable trait vs raw deploy: gas used deploy trait 1`] = `4548`; exports[`benchmarks benchmark deployable trait vs raw deploy: gas used raw deploy 1`] = `1585`; -exports[`benchmarks benchmark functions: code size 1`] = `184`; +exports[`benchmarks benchmark functions: code size 1`] = `183`; -exports[`benchmarks benchmark functions: gas used 1`] = `2236`; +exports[`benchmarks benchmark functions: gas used 1`] = `2208`; exports[`benchmarks benchmark readFwdFee: code size 1`] = `120`; -exports[`benchmarks benchmark readFwdFee: gas used 1`] = `2058`; +exports[`benchmarks benchmark readFwdFee: gas used 1`] = `2048`; exports[`benchmarks benchmark sha256: gas hash string big 1`] = `2562`; diff --git a/src/test/benchmarks/escrow/results.json b/src/test/benchmarks/escrow/results.json index e3582650d..026a8879c 100644 --- a/src/test/benchmarks/escrow/results.json +++ b/src/test/benchmarks/escrow/results.json @@ -99,6 +99,16 @@ "approveTon": "9418", "cancelTon": "7144" } + }, + { + "label": "1.5.3 with fixed routing", + "pr": "https://github.com/tact-lang/tact/pull/1949", + "gas": { + "fundingTon": "4988", + "changeCode": "5194", + "approveTon": "9390", + "cancelTon": "7116" + } } ] } diff --git a/src/test/benchmarks/jetton/results.json b/src/test/benchmarks/jetton/results.json index 1474bd79c..1092a844e 100644 --- a/src/test/benchmarks/jetton/results.json +++ b/src/test/benchmarks/jetton/results.json @@ -224,6 +224,15 @@ "burn": "12123", "discovery": "7052" } + }, + { + "label": "1.5.3 with fixed routing", + "pr": "https://github.com/tact-lang/tact/pull/1949", + "gas": { + "transfer": "16036", + "burn": "12067", + "discovery": "7024" + } } ] } diff --git a/src/test/e2e-emulated/contracts/receiver-precedence.tact b/src/test/e2e-emulated/contracts/receiver-precedence.tact index 8527e8ccf..82d374aae 100644 --- a/src/test/e2e-emulated/contracts/receiver-precedence.tact +++ b/src/test/e2e-emulated/contracts/receiver-precedence.tact @@ -1,7 +1,9 @@ -message Message { +message(100) Message { msg: String; } +message(101) Empty { } + message BinaryIntOperation { op: String; val1: Int; @@ -12,6 +14,11 @@ message BinaryIntResult { val: Int; } +message SendCellToAddress { + address: Address; + body: Cell; +} + // This contract receives binary arithmetic requests. // It only supports divisions. contract Calculator { @@ -158,8 +165,1286 @@ contract ReceiverTester { } } + // Initiate a request to the specified address with the specified cell as message body + receive(msg: SendCellToAddress) { + send(SendParameters { + to: msg.address, + bounce: false, + value: ton("5"), + body: msg.body, + }); + } + get fun receiverKind(): String { return self.receiverKind; } } +// This contract does not process messages (no receiver of any kind) +contract NoReceiverTester { + receiver: String = "unknown"; + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only processes empty messages +contract EmptyReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only processes messages with a specific comment +contract CommentReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only processes messages with strings (i.e., any comment) +contract StringReceiverTester { + receiver: String = "unknown"; + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only has the binary message receivers +contract BinaryReceiverTester { + receiver: String = "unknown"; + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only has the fallback receiver +contract SliceReceiverTester { + receiver: String = "unknown"; + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty and specific comment receivers +contract EmptyAndCommentReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty and fallback string receivers +contract EmptyAndStringReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty and binary message receivers +contract EmptyAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty and fallback receivers +contract EmptyAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment and fallback string receivers +contract CommentAndStringReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment and binary message receivers +contract CommentAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment and fallback receivers +contract CommentAndSliceReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the fallback string and binary message receivers +contract StringAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the fallback string and fallback receivers +contract StringAndSliceReceiverTester { + receiver: String = "unknown"; + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the binary message and fallback receivers +contract BinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment and fallback string receivers +contract EmptyAndCommentAndStringReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment and binary string receivers +contract EmptyAndCommentAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment and fallback receivers +contract EmptyAndCommentAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, fallback string and binary receivers +contract EmptyAndStringAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, fallback string and fallback receivers +contract EmptyAndStringAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, binary and fallback receivers +contract EmptyAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment, string and binary receivers +contract CommentAndStringAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment, string and fallback receivers +contract CommentAndStringAndSliceReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment, binary and fallback receivers +contract CommentAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the string, binary and fallback receivers +contract StringAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment, fallback string, and binary receivers +contract EmptyAndCommentAndStringAndBinaryReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment, fallback string, and fallback receivers +contract EmptyAndCommentAndStringAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, specific comment, binary, and fallback receivers +contract EmptyAndCommentAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the empty, string, binary and fallback receivers +contract EmptyAndStringAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has the specific comment, string, binary and fallback receivers +contract CommentAndStringAndBinaryAndSliceReceiverTester { + receiver: String = "unknown"; + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract has all the receiver kinds +contract AllReceiverTester { + receiver: String = "unknown"; + + receive() { + self.receiver = "empty"; + } + + receive("message") { + self.receiver = "comment"; + } + + receive(msg: String) { + self.receiver = "fallback_string"; + } + + receive(msg: Message) { + self.receiver = "binary"; + } + + receive(msg: Slice) { + self.receiver = "fallback"; + } + + receive(msg: Empty) { + self.receiver = "binary_empty_message"; + } + + external(msg: Empty) { + acceptMessage(); + self.receiver = "external_binary_empty_message"; + } + + external() { + acceptMessage(); + self.receiver = "external_empty"; + } + + external("message") { + acceptMessage(); + self.receiver = "external_comment"; + } + + external(msg: String) { + acceptMessage(); + self.receiver = "external_fallback_string"; + } + + external(msg: Message) { + acceptMessage(); + self.receiver = "external_binary"; + } + + external(msg: Slice) { + acceptMessage(); + self.receiver = "external_fallback"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract does not process bounced messages +contract EmptyBouncedTester { + receiver: String = "unknown"; + + // Initiate a request to the specified address with the specified cell as message body + receive(msg: SendCellToAddress) { + send(SendParameters { + to: msg.address, + bounce: true, + value: ton("5"), + body: msg.body, + }); + } + + receive("reset") { + self.receiver = "unknown"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only processes binary bounced messages +contract BinaryBouncedTester { + receiver: String = "unknown"; + + bounced(msg: bounced) { + self.receiver = "bounced_binary" + } + + bounced(msg: Empty) { + self.receiver = "bounced_binary_empty_message" + } + + // Initiate a request to the specified address with the specified cell as message body + receive(msg: SendCellToAddress) { + send(SendParameters { + to: msg.address, + bounce: true, + value: ton("5"), + body: msg.body, + }); + } + + receive("reset") { + self.receiver = "unknown"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract only processes slice bounced messages +contract SliceBouncedTester { + receiver: String = "unknown"; + + bounced(msg: Slice) { + self.receiver = "bounced_fallback" + } + + // Initiate a request to the specified address with the specified cell as message body + receive(msg: SendCellToAddress) { + send(SendParameters { + to: msg.address, + bounce: true, + value: ton("5"), + body: msg.body, + }); + } + + receive("reset") { + self.receiver = "unknown"; + } + + get fun receiver(): String { + return self.receiver; + } +} + +// This contract process all kinds of bounced messages +contract AllBouncedTester { + receiver: String = "unknown"; + + bounced(msg: bounced) { + self.receiver = "bounced_binary" + } + + bounced(msg: Slice) { + self.receiver = "bounced_fallback" + } + + bounced(msg: Empty) { + self.receiver = "bounced_binary_empty_message" + } + + // Initiate a request to the specified address with the specified cell as message body + receive(msg: SendCellToAddress) { + send(SendParameters { + to: msg.address, + bounce: true, + value: ton("5"), + body: msg.body, + }); + } + + receive("reset") { + self.receiver = "unknown"; + } + + get fun receiver(): String { + return self.receiver; + } +} + diff --git a/src/test/e2e-emulated/receiver-precedence.spec.ts b/src/test/e2e-emulated/receiver-precedence.spec.ts index 0f619aac6..dc1ce4bc0 100644 --- a/src/test/e2e-emulated/receiver-precedence.spec.ts +++ b/src/test/e2e-emulated/receiver-precedence.spec.ts @@ -1,9 +1,52 @@ -import { beginCell, Cell, toNano } from "@ton/core"; -import type { SandboxContract, TreasuryContract } from "@ton/sandbox"; -import { Blockchain } from "@ton/sandbox"; +import type { Address, Sender, TransactionDescriptionGeneric } from "@ton/core"; +import { beginCell, Cell, external, toNano } from "@ton/core"; +import type { + SandboxContract, + SendMessageResult, + SmartContractTransaction, + TreasuryContract, +} from "@ton/sandbox"; +import { Blockchain, internal } from "@ton/sandbox"; import { ReceiverTester } from "./contracts/output/receiver-precedence_ReceiverTester"; import { Calculator } from "./contracts/output/receiver-precedence_Calculator"; import "@ton/test-utils"; +import { AllReceiverTester } from "./contracts/output/receiver-precedence_AllReceiverTester"; +import { BinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_BinaryAndSliceReceiverTester"; +import { BinaryReceiverTester } from "./contracts/output/receiver-precedence_BinaryReceiverTester"; +import { CommentAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_CommentAndBinaryAndSliceReceiverTester"; +import { CommentAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_CommentAndBinaryReceiverTester"; +import { CommentAndSliceReceiverTester } from "./contracts/output/receiver-precedence_CommentAndSliceReceiverTester"; +import { CommentAndStringAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_CommentAndStringAndBinaryAndSliceReceiverTester"; +import { CommentAndStringAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_CommentAndStringAndBinaryReceiverTester"; +import { CommentAndStringAndSliceReceiverTester } from "./contracts/output/receiver-precedence_CommentAndStringAndSliceReceiverTester"; +import { CommentAndStringReceiverTester } from "./contracts/output/receiver-precedence_CommentAndStringReceiverTester"; +import { CommentReceiverTester } from "./contracts/output/receiver-precedence_CommentReceiverTester"; +import { EmptyAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndBinaryAndSliceReceiverTester"; +import { EmptyAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndBinaryReceiverTester"; +import { EmptyAndCommentAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndBinaryAndSliceReceiverTester"; +import { EmptyAndCommentAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndBinaryReceiverTester"; +import { EmptyAndCommentAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndSliceReceiverTester"; +import { EmptyAndCommentAndStringAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndStringAndBinaryReceiverTester"; +import { EmptyAndCommentAndStringAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndStringAndSliceReceiverTester"; +import { EmptyAndCommentAndStringReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentAndStringReceiverTester"; +import { EmptyAndCommentReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndCommentReceiverTester"; +import { EmptyAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndSliceReceiverTester"; +import { EmptyAndStringAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndStringAndBinaryAndSliceReceiverTester"; +import { EmptyAndStringAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndStringAndBinaryReceiverTester"; +import { EmptyAndStringAndSliceReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndStringAndSliceReceiverTester"; +import { EmptyAndStringReceiverTester } from "./contracts/output/receiver-precedence_EmptyAndStringReceiverTester"; +import { EmptyReceiverTester } from "./contracts/output/receiver-precedence_EmptyReceiverTester"; +import { NoReceiverTester } from "./contracts/output/receiver-precedence_NoReceiverTester"; +import { SliceReceiverTester } from "./contracts/output/receiver-precedence_SliceReceiverTester"; +import { StringAndBinaryAndSliceReceiverTester } from "./contracts/output/receiver-precedence_StringAndBinaryAndSliceReceiverTester"; +import { StringAndBinaryReceiverTester } from "./contracts/output/receiver-precedence_StringAndBinaryReceiverTester"; +import { StringAndSliceReceiverTester } from "./contracts/output/receiver-precedence_StringAndSliceReceiverTester"; +import { StringReceiverTester } from "./contracts/output/receiver-precedence_StringReceiverTester"; +import { AllBouncedTester } from "./contracts/output/receiver-precedence_AllBouncedTester"; +import { EmptyBouncedTester } from "./contracts/output/receiver-precedence_EmptyBouncedTester"; +import { BinaryBouncedTester } from "./contracts/output/receiver-precedence_BinaryBouncedTester"; +import { SliceBouncedTester } from "./contracts/output/receiver-precedence_SliceBouncedTester"; +import type { SendCellToAddress } from "./contracts/output/receiver-precedence_EmptyBouncedTester"; describe("receivers-precedence", () => { let blockchain: Blockchain; @@ -11,6 +54,45 @@ describe("receivers-precedence", () => { let contract: SandboxContract; let calculator: SandboxContract; + // All combinations of contracts with receivers + let noReceivers: SandboxContract; + let emptyReceiver: SandboxContract; + let commentReceiver: SandboxContract; + let stringReceiver: SandboxContract; + let binaryReceiver: SandboxContract; + let sliceReceiver: SandboxContract; + let emptyAndCommentReceiver: SandboxContract; + let emptyAndStringReceiver: SandboxContract; + let emptyAndBinaryReceiver: SandboxContract; + let emptyAndSliceReceiver: SandboxContract; + let commentAndStringReceiver: SandboxContract; + let commentAndBinaryReceiver: SandboxContract; + let commentAndSliceReceiver: SandboxContract; + let stringAndBinaryReceiver: SandboxContract; + let stringAndSliceReceiver: SandboxContract; + let binaryAndSliceReceiver: SandboxContract; + let emptyAndCommentAndStringReceiver: SandboxContract; + let emptyAndCommentAndBinaryReceiver: SandboxContract; + let emptyAndCommentAndSliceReceiver: SandboxContract; + let emptyAndStringAndBinaryReceiver: SandboxContract; + let emptyAndStringAndSliceReceiver: SandboxContract; + let emptyAndBinaryAndSliceReceiver: SandboxContract; + let commentAndStringAndBinaryReceiver: SandboxContract; + let commentAndStringAndSliceReceiver: SandboxContract; + let commentAndBinaryAndSliceReceiver: SandboxContract; + let stringAndBinaryAndSliceReceiver: SandboxContract; + let emptyAndCommentAndStringAndBinaryReceiver: SandboxContract; + let emptyAndCommentAndStringAndSliceReceiver: SandboxContract; + let emptyAndCommentAndBinaryAndSliceReceiver: SandboxContract; + let emptyAndStringAndBinaryAndSliceReceiver: SandboxContract; + let commentAndStringAndBinaryAndSliceReceiver: SandboxContract; + let allReceivers: SandboxContract; + + let emptyBounced: SandboxContract; + let binaryBounced: SandboxContract; + let sliceBounced: SandboxContract; + let allBounced: SandboxContract; + beforeEach(async () => { blockchain = await Blockchain.create(); blockchain.verbosity.print = false; @@ -19,6 +101,115 @@ describe("receivers-precedence", () => { contract = blockchain.openContract(await ReceiverTester.fromInit()); calculator = blockchain.openContract(await Calculator.fromInit()); + // All receiver contracts + noReceivers = blockchain.openContract( + await NoReceiverTester.fromInit(), + ); + emptyReceiver = blockchain.openContract( + await EmptyReceiverTester.fromInit(), + ); + commentReceiver = blockchain.openContract( + await CommentReceiverTester.fromInit(), + ); + stringReceiver = blockchain.openContract( + await StringReceiverTester.fromInit(), + ); + binaryReceiver = blockchain.openContract( + await BinaryReceiverTester.fromInit(), + ); + sliceReceiver = blockchain.openContract( + await SliceReceiverTester.fromInit(), + ); + emptyAndCommentReceiver = blockchain.openContract( + await EmptyAndCommentReceiverTester.fromInit(), + ); + emptyAndStringReceiver = blockchain.openContract( + await EmptyAndStringReceiverTester.fromInit(), + ); + emptyAndBinaryReceiver = blockchain.openContract( + await EmptyAndBinaryReceiverTester.fromInit(), + ); + emptyAndSliceReceiver = blockchain.openContract( + await EmptyAndSliceReceiverTester.fromInit(), + ); + commentAndStringReceiver = blockchain.openContract( + await CommentAndStringReceiverTester.fromInit(), + ); + commentAndBinaryReceiver = blockchain.openContract( + await CommentAndBinaryReceiverTester.fromInit(), + ); + commentAndSliceReceiver = blockchain.openContract( + await CommentAndSliceReceiverTester.fromInit(), + ); + stringAndBinaryReceiver = blockchain.openContract( + await StringAndBinaryReceiverTester.fromInit(), + ); + stringAndSliceReceiver = blockchain.openContract( + await StringAndSliceReceiverTester.fromInit(), + ); + binaryAndSliceReceiver = blockchain.openContract( + await BinaryAndSliceReceiverTester.fromInit(), + ); + emptyAndCommentAndStringReceiver = blockchain.openContract( + await EmptyAndCommentAndStringReceiverTester.fromInit(), + ); + emptyAndCommentAndBinaryReceiver = blockchain.openContract( + await EmptyAndCommentAndBinaryReceiverTester.fromInit(), + ); + emptyAndCommentAndSliceReceiver = blockchain.openContract( + await EmptyAndCommentAndSliceReceiverTester.fromInit(), + ); + emptyAndStringAndBinaryReceiver = blockchain.openContract( + await EmptyAndStringAndBinaryReceiverTester.fromInit(), + ); + emptyAndStringAndSliceReceiver = blockchain.openContract( + await EmptyAndStringAndSliceReceiverTester.fromInit(), + ); + emptyAndBinaryAndSliceReceiver = blockchain.openContract( + await EmptyAndBinaryAndSliceReceiverTester.fromInit(), + ); + commentAndStringAndBinaryReceiver = blockchain.openContract( + await CommentAndStringAndBinaryReceiverTester.fromInit(), + ); + commentAndStringAndSliceReceiver = blockchain.openContract( + await CommentAndStringAndSliceReceiverTester.fromInit(), + ); + commentAndBinaryAndSliceReceiver = blockchain.openContract( + await CommentAndBinaryAndSliceReceiverTester.fromInit(), + ); + stringAndBinaryAndSliceReceiver = blockchain.openContract( + await StringAndBinaryAndSliceReceiverTester.fromInit(), + ); + emptyAndCommentAndStringAndBinaryReceiver = blockchain.openContract( + await EmptyAndCommentAndStringAndBinaryReceiverTester.fromInit(), + ); + emptyAndCommentAndStringAndSliceReceiver = blockchain.openContract( + await EmptyAndCommentAndStringAndSliceReceiverTester.fromInit(), + ); + emptyAndCommentAndBinaryAndSliceReceiver = blockchain.openContract( + await EmptyAndCommentAndBinaryAndSliceReceiverTester.fromInit(), + ); + emptyAndStringAndBinaryAndSliceReceiver = blockchain.openContract( + await EmptyAndStringAndBinaryAndSliceReceiverTester.fromInit(), + ); + commentAndStringAndBinaryAndSliceReceiver = blockchain.openContract( + await CommentAndStringAndBinaryAndSliceReceiverTester.fromInit(), + ); + allReceivers = blockchain.openContract( + await AllReceiverTester.fromInit(), + ); + + emptyBounced = blockchain.openContract( + await EmptyBouncedTester.fromInit(), + ); + binaryBounced = blockchain.openContract( + await BinaryBouncedTester.fromInit(), + ); + sliceBounced = blockchain.openContract( + await SliceBouncedTester.fromInit(), + ); + allBounced = blockchain.openContract(await AllBouncedTester.fromInit()); + const deployResult = await contract.send( treasure.getSender(), { value: toNano("10") }, @@ -42,6 +233,138 @@ describe("receivers-precedence", () => { success: true, deploy: true, }); + + // Deploy contracts + await deploy(noReceivers.address, await NoReceiverTester.init()); + await deploy(emptyReceiver.address, await EmptyReceiverTester.init()); + await deploy( + commentReceiver.address, + await CommentReceiverTester.init(), + ); + await deploy(stringReceiver.address, await StringReceiverTester.init()); + await deploy(binaryReceiver.address, await BinaryReceiverTester.init()); + await deploy(sliceReceiver.address, await SliceReceiverTester.init()); + await deploy( + emptyAndCommentReceiver.address, + await EmptyAndCommentReceiverTester.init(), + ); + await deploy( + emptyAndStringReceiver.address, + await EmptyAndStringReceiverTester.init(), + ); + await deploy( + emptyAndBinaryReceiver.address, + await EmptyAndBinaryReceiverTester.init(), + ); + await deploy( + emptyAndSliceReceiver.address, + await EmptyAndSliceReceiverTester.init(), + ); + await deploy( + commentAndStringReceiver.address, + await CommentAndStringReceiverTester.init(), + ); + await deploy( + commentAndBinaryReceiver.address, + await CommentAndBinaryReceiverTester.init(), + ); + await deploy( + commentAndSliceReceiver.address, + await CommentAndSliceReceiverTester.init(), + ); + await deploy( + stringAndBinaryReceiver.address, + await StringAndBinaryReceiverTester.init(), + ); + await deploy( + stringAndSliceReceiver.address, + await StringAndSliceReceiverTester.init(), + ); + await deploy( + binaryAndSliceReceiver.address, + await BinaryAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndStringReceiver.address, + await EmptyAndCommentAndStringReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndBinaryReceiver.address, + await EmptyAndCommentAndBinaryReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndSliceReceiver.address, + await EmptyAndCommentAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndStringAndBinaryReceiver.address, + await EmptyAndStringAndBinaryReceiverTester.init(), + ); + await deploy( + emptyAndStringAndSliceReceiver.address, + await EmptyAndStringAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndBinaryAndSliceReceiver.address, + await EmptyAndBinaryAndSliceReceiverTester.init(), + ); + await deploy( + commentAndStringAndBinaryReceiver.address, + await CommentAndStringAndBinaryReceiverTester.init(), + ); + await deploy( + commentAndStringAndSliceReceiver.address, + await CommentAndStringAndSliceReceiverTester.init(), + ); + await deploy( + commentAndBinaryAndSliceReceiver.address, + await CommentAndBinaryAndSliceReceiverTester.init(), + ); + await deploy( + stringAndBinaryAndSliceReceiver.address, + await StringAndBinaryAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndStringAndBinaryReceiver.address, + await EmptyAndCommentAndStringAndBinaryReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndStringAndSliceReceiver.address, + await EmptyAndCommentAndStringAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndCommentAndBinaryAndSliceReceiver.address, + await EmptyAndCommentAndBinaryAndSliceReceiverTester.init(), + ); + await deploy( + emptyAndStringAndBinaryAndSliceReceiver.address, + await EmptyAndStringAndBinaryAndSliceReceiverTester.init(), + ); + await deploy( + commentAndStringAndBinaryAndSliceReceiver.address, + await CommentAndStringAndBinaryAndSliceReceiverTester.init(), + ); + await deploy(allReceivers.address, await AllReceiverTester.init()); + + await deploy(emptyBounced.address, await EmptyBouncedTester.init()); + await deploy(binaryBounced.address, await BinaryBouncedTester.init()); + await deploy(sliceBounced.address, await SliceBouncedTester.init()); + await deploy(allBounced.address, await AllBouncedTester.init()); + + async function deploy(addr: Address, init: { code: Cell; data: Cell }) { + const deployable = await blockchain.getContract(addr); + const trans = await deployable.receiveMessage( + internal({ + from: treasure.address, + to: deployable.address, + value: toNano("10"), + stateInit: init, + bounce: false, + }), + ); + + expect(trans.endStatus).toBe("active"); + } }); it("should implement receivers precedence correctly", async () => { @@ -288,4 +611,1724 @@ describe("receivers-precedence", () => { // In all the cases, "external_error_comment" did not execute, as it should be. }); + + it("internal receivers should process empty messages and empty strings correctly", async () => { + // A message struct with empty string inside. Should only be accepted by binary receivers + const emptyStringInMessageStruct = beginCell() + .storeUint(100, 32) + .storeStringRefTail("") + .endCell(); + // An empty message struct, should only be accepted by binary receivers. + const emptyMessageStruct = beginCell().storeUint(101, 32).endCell(); + // Message bodies with integer of size less than 32 bits will be processed by empty receivers (if present), + // irrespective of the value of the integer + const lessThan32Bits = beginCell().storeUint(10, 30).endCell(); + // An actual empty message body + const emptyBody = new Cell(); + // Message bodies with integers of size exactly 32 bits but value 0 will be processed by empty receivers (if present). + const zeroOf32Bits = beginCell().storeUint(0, 32).endCell(); + // The empty string will be processed by empty receivers (if present) + const emptyString = beginCell() + .storeUint(0, 32) + .storeStringTail("") + .endCell(); + + const bodiesToTry = [ + emptyStringInMessageStruct, + emptyMessageStruct, + lessThan32Bits, + emptyBody, + zeroOf32Bits, + emptyString, + ]; + + // Some utility functions that carry out the actual tests and assertions + + async function shouldFailFrom( + testedContract: Address, + from: number, + exitCode: number, + ) { + for (const body of bodiesToTry.slice(from)) { + await shouldFailBody(testedContract, exitCode, body); + } + } + + async function shouldAcceptFrom( + testedContract: Address, + from: number, + receiverGetter: () => Promise, + expectedRestReceiver: string, + ) { + for (const body of bodiesToTry.slice(from)) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedRestReceiver, + body, + ); + } + } + + async function shouldFailIncompleteOpCode( + testedContract: Address, + exitCode: number, + ) { + await shouldFailBody(testedContract, exitCode, lessThan32Bits); + } + + async function shouldFailEmptyBody( + testedContract: Address, + exitCode: number, + ) { + await shouldFailBody(testedContract, exitCode, emptyBody); + } + + async function shouldFailMessageStruct( + testedContract: Address, + exitCode: number, + ) { + await shouldFailBody( + testedContract, + exitCode, + emptyStringInMessageStruct, + ); + } + + async function shouldFailEmptyMessageStruct( + testedContract: Address, + exitCode: number, + ) { + await shouldFailBody(testedContract, exitCode, emptyMessageStruct); + } + + async function shouldAcceptIncompleteOpCode( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + lessThan32Bits, + ); + } + + async function shouldAcceptEmptyBody( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyBody, + ); + } + + async function shouldAcceptMessageStruct( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyStringInMessageStruct, + ); + } + + async function shouldAcceptEmptyMessageStruct( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyMessageStruct, + ); + } + + async function shouldAcceptBody( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + body: Cell, + ) { + const { transactions } = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + { + $$type: "SendCellToAddress", + address: testedContract, + body: body, + }, + ); + + expect(transactions).toHaveTransaction({ + from: contract.address, + to: testedContract, + success: true, + }); + expect(await receiverGetter()).toBe(expectedReceiver); + } + + async function shouldFailBody( + testedContract: Address, + exitCode: number, + body: Cell, + ) { + const { transactions } = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + { + $$type: "SendCellToAddress", + address: testedContract, + body: body, + }, + ); + + expect(transactions).toHaveTransaction({ + from: contract.address, + to: testedContract, + success: false, + exitCode: exitCode, + }); + } + + // Tests start here + + // noReceivers should fail in all the cases with exit code 130 + await shouldFailFrom(noReceivers.address, 0, 130); + + // emptyReceiver + // Should fail on message structs + // And accept the rest + await shouldFailMessageStruct(emptyReceiver.address, 130); + await shouldFailEmptyMessageStruct(emptyReceiver.address, 130); + await shouldAcceptFrom( + emptyReceiver.address, + 2, + emptyReceiver.getReceiver, + "empty", + ); + + // commentReceiver should fail in all the cases with exit code 130 + await shouldFailFrom(commentReceiver.address, 0, 130); + + // stringReceiver should accept from 4 onwards on string receiver + await shouldFailMessageStruct(stringReceiver.address, 130); + await shouldFailEmptyMessageStruct(stringReceiver.address, 130); + await shouldFailIncompleteOpCode(stringReceiver.address, 130); + await shouldFailEmptyBody(stringReceiver.address, 130); + await shouldAcceptFrom( + stringReceiver.address, + 4, + stringReceiver.getReceiver, + "fallback_string", + ); + + // binaryReceiver + // Accepts binary cases + // and fails in the rest with exit code 130 + await shouldAcceptMessageStruct( + binaryReceiver.address, + binaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + binaryReceiver.address, + binaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldFailFrom(binaryReceiver.address, 2, 130); + + // sliceReceiver should accept all the cases + await shouldAcceptFrom( + sliceReceiver.address, + 0, + sliceReceiver.getReceiver, + "fallback", + ); + + // emptyAndCommentReceiver + // Fails in the message structs + // and accepts the rest of cases in the empty receiver + await shouldFailMessageStruct(emptyAndCommentReceiver.address, 130); + await shouldFailEmptyMessageStruct( + emptyAndCommentReceiver.address, + 130, + ); + await shouldAcceptFrom( + emptyAndCommentReceiver.address, + 2, + emptyAndCommentReceiver.getReceiver, + "empty", + ); + + // emptyAndStringReceiver + // Fails in the message structs + // and accepts the rest of cases in the empty receiver + await shouldFailMessageStruct(emptyAndStringReceiver.address, 130); + await shouldFailEmptyMessageStruct(emptyAndStringReceiver.address, 130); + await shouldAcceptFrom( + emptyAndStringReceiver.address, + 2, + emptyAndStringReceiver.getReceiver, + "empty", + ); + + // emptyAndBinaryReceiver + // Accepts the message structs in the binary receiver + // and accepts the rest of cases in the empty receiver + await shouldAcceptMessageStruct( + emptyAndBinaryReceiver.address, + emptyAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndBinaryReceiver.address, + emptyAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndBinaryReceiver.address, + 2, + emptyAndBinaryReceiver.getReceiver, + "empty", + ); + + // emptyAndSliceReceiver + // Accepts the message structs in the fallback receiver + // and accepts the rest of cases in the empty receiver + await shouldAcceptMessageStruct( + emptyAndSliceReceiver.address, + emptyAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndSliceReceiver.address, + emptyAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + emptyAndSliceReceiver.address, + 2, + emptyAndSliceReceiver.getReceiver, + "empty", + ); + + // commentAndStringReceiver accepts from 4 onwards on string receiver + await shouldFailMessageStruct(commentAndStringReceiver.address, 130); + await shouldFailEmptyMessageStruct( + commentAndStringReceiver.address, + 130, + ); + await shouldFailIncompleteOpCode(commentAndStringReceiver.address, 130); + await shouldFailEmptyBody(commentAndStringReceiver.address, 130); + await shouldAcceptFrom( + commentAndStringReceiver.address, + 4, + commentAndStringReceiver.getReceiver, + "fallback_string", + ); + + // commentAndBinaryReceiver + // Accepts the message structs in the binary receiver + // and fails in the rest + await shouldAcceptMessageStruct( + commentAndBinaryReceiver.address, + commentAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndBinaryReceiver.address, + commentAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldFailFrom(commentAndBinaryReceiver.address, 2, 130); + + // commentAndSliceReceiver should accept all the cases in the fallback receiver + await shouldAcceptFrom( + commentAndSliceReceiver.address, + 0, + commentAndSliceReceiver.getReceiver, + "fallback", + ); + + // stringAndBinaryReceiver + // should accepts structs in binary receivers. + // accepts from 4 onwards in the string receiver + await shouldAcceptMessageStruct( + stringAndBinaryReceiver.address, + stringAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + stringAndBinaryReceiver.address, + stringAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldFailIncompleteOpCode(stringAndBinaryReceiver.address, 130); + await shouldFailEmptyBody(stringAndBinaryReceiver.address, 130); + await shouldAcceptFrom( + stringAndBinaryReceiver.address, + 4, + stringAndBinaryReceiver.getReceiver, + "fallback_string", + ); + + // stringAndSliceReceiver should accept firsts in fallback receiver + // and the rest from 4 onwards in the string receiver + await shouldAcceptMessageStruct( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptIncompleteOpCode( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyBody( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + stringAndSliceReceiver.address, + 4, + stringAndSliceReceiver.getReceiver, + "fallback_string", + ); + + // binaryAndSliceReceiver + // should accept structs in binary receivers and rest in the fallback receiver + await shouldAcceptMessageStruct( + binaryAndSliceReceiver.address, + binaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + binaryAndSliceReceiver.address, + binaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + binaryAndSliceReceiver.address, + 2, + binaryAndSliceReceiver.getReceiver, + "fallback", + ); + + // emptyAndCommentAndStringReceiver + // should fail on message structs + // but accept all the rest in the empty receiver + await shouldFailMessageStruct( + emptyAndCommentAndStringReceiver.address, + 130, + ); + await shouldFailEmptyMessageStruct( + emptyAndCommentAndStringReceiver.address, + 130, + ); + await shouldAcceptFrom( + emptyAndCommentAndStringReceiver.address, + 2, + emptyAndCommentAndStringReceiver.getReceiver, + "empty", + ); + + // emptyAndCommentAndBinaryReceiver + // should accept structs in binary receivers and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndCommentAndBinaryReceiver.address, + emptyAndCommentAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndBinaryReceiver.address, + emptyAndCommentAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndBinaryReceiver.address, + 2, + emptyAndCommentAndBinaryReceiver.getReceiver, + "empty", + ); + + // emptyAndCommentAndSliceReceiver + // should accept the structs in fallback and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndCommentAndSliceReceiver.address, + emptyAndCommentAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndSliceReceiver.address, + emptyAndCommentAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + emptyAndCommentAndSliceReceiver.address, + 2, + emptyAndCommentAndSliceReceiver.getReceiver, + "empty", + ); + + // emptyAndStringAndBinaryReceiver + // should accept structs in binary and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndStringAndBinaryReceiver.address, + emptyAndStringAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndBinaryReceiver.address, + emptyAndStringAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndStringAndBinaryReceiver.address, + 2, + emptyAndStringAndBinaryReceiver.getReceiver, + "empty", + ); + + // emptyAndStringAndSliceReceiver + // should accept structs in fallback and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndStringAndSliceReceiver.address, + emptyAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndSliceReceiver.address, + emptyAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + emptyAndStringAndSliceReceiver.address, + 2, + emptyAndStringAndSliceReceiver.getReceiver, + "empty", + ); + + // emptyAndBinaryAndSliceReceiver + // should accept structs in binary and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndBinaryAndSliceReceiver.address, + emptyAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndBinaryAndSliceReceiver.address, + emptyAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndBinaryAndSliceReceiver.address, + 2, + emptyAndBinaryAndSliceReceiver.getReceiver, + "empty", + ); + + // commentAndStringAndBinaryReceiver + // should accept structs in binary receivers + // and accept from 4 onwards at the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndBinaryReceiver.address, + commentAndStringAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndBinaryReceiver.address, + commentAndStringAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldFailIncompleteOpCode( + commentAndStringAndBinaryReceiver.address, + 130, + ); + await shouldFailEmptyBody( + commentAndStringAndBinaryReceiver.address, + 130, + ); + await shouldAcceptFrom( + commentAndStringAndBinaryReceiver.address, + 4, + commentAndStringAndBinaryReceiver.getReceiver, + "fallback_string", + ); + + // commentAndStringAndSliceReceiver + // should accept firsts in fallback + // but accept the rest in the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptIncompleteOpCode( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyBody( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + commentAndStringAndSliceReceiver.address, + 4, + commentAndStringAndSliceReceiver.getReceiver, + "fallback_string", + ); + + // commentAndBinaryAndSliceReceiver + // should accept structs in binary + // should accept the rest in the fallback receiver + await shouldAcceptMessageStruct( + commentAndBinaryAndSliceReceiver.address, + commentAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndBinaryAndSliceReceiver.address, + commentAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + commentAndBinaryAndSliceReceiver.address, + 2, + commentAndBinaryAndSliceReceiver.getReceiver, + "fallback", + ); + + // stringAndBinaryAndSliceReceiver + // should accept the structs in binary + // should accept middle ones in fallback + // and the rest in the string receiver + await shouldAcceptMessageStruct( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptIncompleteOpCode( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyBody( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + stringAndBinaryAndSliceReceiver.address, + 4, + stringAndBinaryAndSliceReceiver.getReceiver, + "fallback_string", + ); + + // emptyAndCommentAndStringAndBinaryReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndStringAndBinaryReceiver.address, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndStringAndBinaryReceiver.address, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndStringAndBinaryReceiver.address, + 2, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "empty", + ); + + // emptyAndCommentAndStringAndSliceReceiver + // structs in fallback + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndStringAndSliceReceiver.address, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndStringAndSliceReceiver.address, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + emptyAndCommentAndStringAndSliceReceiver.address, + 2, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "empty", + ); + + // emptyAndCommentAndBinaryAndSliceReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndBinaryAndSliceReceiver.address, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndBinaryAndSliceReceiver.address, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndBinaryAndSliceReceiver.address, + 2, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "empty", + ); + + // emptyAndStringAndBinaryAndSliceReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndStringAndBinaryAndSliceReceiver.address, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndBinaryAndSliceReceiver.address, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndStringAndBinaryAndSliceReceiver.address, + 2, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "empty", + ); + + // commentAndStringAndBinaryAndSliceReceiver + // structs in binary + // middle ones in the fallback receiver, + // and the rest in the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "binary_empty_message", + ); + await shouldAcceptIncompleteOpCode( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptEmptyBody( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "fallback", + ); + await shouldAcceptFrom( + commentAndStringAndBinaryAndSliceReceiver.address, + 4, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "fallback_string", + ); + + // allReceivers + // structs in binary + // the rest in the empty receiver + await shouldAcceptMessageStruct( + allReceivers.address, + allReceivers.getReceiver, + "binary", + ); + await shouldAcceptEmptyMessageStruct( + allReceivers.address, + allReceivers.getReceiver, + "binary_empty_message", + ); + await shouldAcceptFrom( + allReceivers.address, + 2, + allReceivers.getReceiver, + "empty", + ); + }); + + it("external receivers should process empty messages and empty strings correctly", async () => { + // A message struct with empty string inside. Should only be accepted by binary receivers + const emptyStringInMessageStruct = beginCell() + .storeUint(100, 32) + .storeStringRefTail("") + .endCell(); + // An empty message struct, should only be accepted by binary receivers. + const emptyMessageStruct = beginCell().storeUint(101, 32).endCell(); + // Message bodies with integer of size less than 32 bits will be processed by empty receivers (if present), + // irrespective of the value of the integer + const lessThan32Bits = beginCell().storeUint(10, 30).endCell(); + // An actual empty message body + const emptyBody = new Cell(); + // Message bodies with integers of size exactly 32 bits but value 0 will be processed by empty receivers (if present). + const zeroOf32Bits = beginCell().storeUint(0, 32).endCell(); + // The empty string will be processed by empty receivers (if present) + const emptyString = beginCell() + .storeUint(0, 32) + .storeStringTail("") + .endCell(); + + const bodiesToTry = [ + emptyStringInMessageStruct, + emptyMessageStruct, + lessThan32Bits, + emptyBody, + zeroOf32Bits, + emptyString, + ]; + + // Some utility functions that carry out the actual tests and assertions + + async function shouldFailFrom(testedContract: Address, from: number) { + for (const body of bodiesToTry.slice(from)) { + await shouldFailBody(testedContract, body); + } + } + + async function shouldAcceptFrom( + testedContract: Address, + from: number, + receiverGetter: () => Promise, + expectedRestReceiver: string, + ) { + for (const body of bodiesToTry.slice(from)) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedRestReceiver, + body, + ); + } + } + + async function shouldFailIncompleteOpCode(testedContract: Address) { + await shouldFailBody(testedContract, lessThan32Bits); + } + + async function shouldFailEmptyBody(testedContract: Address) { + await shouldFailBody(testedContract, emptyBody); + } + + async function shouldFailMessageStruct(testedContract: Address) { + await shouldFailBody(testedContract, emptyStringInMessageStruct); + } + + async function shouldFailEmptyMessageStruct(testedContract: Address) { + await shouldFailBody(testedContract, emptyMessageStruct); + } + + async function shouldAcceptIncompleteOpCode( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + lessThan32Bits, + ); + } + + async function shouldAcceptEmptyBody( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyBody, + ); + } + + async function shouldAcceptMessageStruct( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyStringInMessageStruct, + ); + } + + async function shouldAcceptEmptyMessageStruct( + testedContract: Address, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + receiverGetter, + expectedReceiver, + emptyMessageStruct, + ); + } + + async function shouldAcceptBody( + testedContract: Address, + receiverGetter: () => Promise, + expectedRestReceiver: string, + body: Cell, + ) { + const contract = await blockchain.getContract(testedContract); + const transaction = await contract.receiveMessage( + external({ + to: contract.address, + body: body, + }), + ); + + const transDesc = getTransactionDescription(transaction); + expect(transDesc.aborted).toBe(false); + expect(await receiverGetter()).toBe(expectedRestReceiver); + } + + async function shouldFailBody(testedContract: Address, body: Cell) { + const contract = await blockchain.getContract(testedContract); + try { + await contract.receiveMessage( + external({ + to: contract.address, + body: body, + }), + ); + + // It should not reach here + expect(false).toBe(true); + } catch (e) { + expect(e instanceof Error).toBe(true); + if (e instanceof Error) { + expect(e.message).toContain( + "External message not accepted by smart contract", + ); + } + } + } + + // Tests start here + + // noReceivers should fail in all the cases + await shouldFailFrom(noReceivers.address, 0); + + // emptyReceiver + // Should fail on message structs + // And accept the rest + await shouldFailMessageStruct(emptyReceiver.address); + await shouldFailEmptyMessageStruct(emptyReceiver.address); + await shouldAcceptFrom( + emptyReceiver.address, + 2, + emptyReceiver.getReceiver, + "external_empty", + ); + + // commentReceiver should fail in all the cases + await shouldFailFrom(commentReceiver.address, 0); + + // stringReceiver should accept from 4 onwards on string receiver + await shouldFailMessageStruct(stringReceiver.address); + await shouldFailEmptyMessageStruct(stringReceiver.address); + await shouldFailIncompleteOpCode(stringReceiver.address); + await shouldFailEmptyBody(stringReceiver.address); + await shouldAcceptFrom( + stringReceiver.address, + 4, + stringReceiver.getReceiver, + "external_fallback_string", + ); + + // binaryReceiver + // Accepts binary cases + // and fails in the rest + await shouldAcceptMessageStruct( + binaryReceiver.address, + binaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + binaryReceiver.address, + binaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldFailFrom(binaryReceiver.address, 2); + + // sliceReceiver should accept all the cases + await shouldAcceptFrom( + sliceReceiver.address, + 0, + sliceReceiver.getReceiver, + "external_fallback", + ); + + // emptyAndCommentReceiver + // Fails in the message structs + // and accepts the rest of cases in the empty receiver + await shouldFailMessageStruct(emptyAndCommentReceiver.address); + await shouldFailEmptyMessageStruct(emptyAndCommentReceiver.address); + await shouldAcceptFrom( + emptyAndCommentReceiver.address, + 2, + emptyAndCommentReceiver.getReceiver, + "external_empty", + ); + + // emptyAndStringReceiver + // Fails in the message structs + // and accepts the rest of cases in the empty receiver + await shouldFailMessageStruct(emptyAndStringReceiver.address); + await shouldFailEmptyMessageStruct(emptyAndStringReceiver.address); + await shouldAcceptFrom( + emptyAndStringReceiver.address, + 2, + emptyAndStringReceiver.getReceiver, + "external_empty", + ); + + // emptyAndBinaryReceiver + // Accepts the message structs in the binary receiver + // and accepts the rest of cases in the empty receiver + await shouldAcceptMessageStruct( + emptyAndBinaryReceiver.address, + emptyAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndBinaryReceiver.address, + emptyAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndBinaryReceiver.address, + 2, + emptyAndBinaryReceiver.getReceiver, + "external_empty", + ); + + // emptyAndSliceReceiver + // Accepts the message structs in the fallback receiver + // and accepts the rest of cases in the empty receiver + await shouldAcceptMessageStruct( + emptyAndSliceReceiver.address, + emptyAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndSliceReceiver.address, + emptyAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + emptyAndSliceReceiver.address, + 2, + emptyAndSliceReceiver.getReceiver, + "external_empty", + ); + + // commentAndStringReceiver accepts from 4 onwards on string receiver + await shouldFailMessageStruct(commentAndStringReceiver.address); + await shouldFailEmptyMessageStruct(commentAndStringReceiver.address); + await shouldFailIncompleteOpCode(commentAndStringReceiver.address); + await shouldFailEmptyBody(commentAndStringReceiver.address); + await shouldAcceptFrom( + commentAndStringReceiver.address, + 4, + commentAndStringReceiver.getReceiver, + "external_fallback_string", + ); + + // commentAndBinaryReceiver + // Accepts the message structs in the binary receiver + // and fails in the rest + await shouldAcceptMessageStruct( + commentAndBinaryReceiver.address, + commentAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndBinaryReceiver.address, + commentAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldFailFrom(commentAndBinaryReceiver.address, 2); + + // commentAndSliceReceiver should accept all the cases in the fallback receiver + await shouldAcceptFrom( + commentAndSliceReceiver.address, + 0, + commentAndSliceReceiver.getReceiver, + "external_fallback", + ); + + // stringAndBinaryReceiver + // should accepts structs in binary receivers. + // accepts from 4 onwards in the string receiver + await shouldAcceptMessageStruct( + stringAndBinaryReceiver.address, + stringAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + stringAndBinaryReceiver.address, + stringAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldFailIncompleteOpCode(stringAndBinaryReceiver.address); + await shouldFailEmptyBody(stringAndBinaryReceiver.address); + await shouldAcceptFrom( + stringAndBinaryReceiver.address, + 4, + stringAndBinaryReceiver.getReceiver, + "external_fallback_string", + ); + + // stringAndSliceReceiver should accept firsts in fallback receiver + // and the rest from 4 onwards in the string receiver + await shouldAcceptMessageStruct( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptIncompleteOpCode( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyBody( + stringAndSliceReceiver.address, + stringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + stringAndSliceReceiver.address, + 4, + stringAndSliceReceiver.getReceiver, + "external_fallback_string", + ); + + // binaryAndSliceReceiver + // should accept structs in binary receivers and rest in the fallback receiver + await shouldAcceptMessageStruct( + binaryAndSliceReceiver.address, + binaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + binaryAndSliceReceiver.address, + binaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + binaryAndSliceReceiver.address, + 2, + binaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + + // emptyAndCommentAndStringReceiver + // should fail on message structs + // but accept all the rest in the empty receiver + await shouldFailMessageStruct(emptyAndCommentAndStringReceiver.address); + await shouldFailEmptyMessageStruct( + emptyAndCommentAndStringReceiver.address, + ); + await shouldAcceptFrom( + emptyAndCommentAndStringReceiver.address, + 2, + emptyAndCommentAndStringReceiver.getReceiver, + "external_empty", + ); + + // emptyAndCommentAndBinaryReceiver + // should accept structs in binary receivers and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndCommentAndBinaryReceiver.address, + emptyAndCommentAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndBinaryReceiver.address, + emptyAndCommentAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndBinaryReceiver.address, + 2, + emptyAndCommentAndBinaryReceiver.getReceiver, + "external_empty", + ); + + // emptyAndCommentAndSliceReceiver + // should accept the structs in fallback and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndCommentAndSliceReceiver.address, + emptyAndCommentAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndSliceReceiver.address, + emptyAndCommentAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + emptyAndCommentAndSliceReceiver.address, + 2, + emptyAndCommentAndSliceReceiver.getReceiver, + "external_empty", + ); + + // emptyAndStringAndBinaryReceiver + // should accept structs in binary and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndStringAndBinaryReceiver.address, + emptyAndStringAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndBinaryReceiver.address, + emptyAndStringAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndStringAndBinaryReceiver.address, + 2, + emptyAndStringAndBinaryReceiver.getReceiver, + "external_empty", + ); + + // emptyAndStringAndSliceReceiver + // should accept structs in fallback and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndStringAndSliceReceiver.address, + emptyAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndSliceReceiver.address, + emptyAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + emptyAndStringAndSliceReceiver.address, + 2, + emptyAndStringAndSliceReceiver.getReceiver, + "external_empty", + ); + + // emptyAndBinaryAndSliceReceiver + // should accept structs in binary and the rest in the empty receiver + await shouldAcceptMessageStruct( + emptyAndBinaryAndSliceReceiver.address, + emptyAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndBinaryAndSliceReceiver.address, + emptyAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndBinaryAndSliceReceiver.address, + 2, + emptyAndBinaryAndSliceReceiver.getReceiver, + "external_empty", + ); + + // commentAndStringAndBinaryReceiver + // should accept structs in binary receivers + // and accept from 4 onwards at the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndBinaryReceiver.address, + commentAndStringAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndBinaryReceiver.address, + commentAndStringAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldFailIncompleteOpCode( + commentAndStringAndBinaryReceiver.address, + ); + await shouldFailEmptyBody(commentAndStringAndBinaryReceiver.address); + await shouldAcceptFrom( + commentAndStringAndBinaryReceiver.address, + 4, + commentAndStringAndBinaryReceiver.getReceiver, + "external_fallback_string", + ); + + // commentAndStringAndSliceReceiver + // should accept firsts in fallback + // but accept the rest in the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptIncompleteOpCode( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyBody( + commentAndStringAndSliceReceiver.address, + commentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + commentAndStringAndSliceReceiver.address, + 4, + commentAndStringAndSliceReceiver.getReceiver, + "external_fallback_string", + ); + + // commentAndBinaryAndSliceReceiver + // should accept structs in binary + // should accept the rest in the fallback receiver + await shouldAcceptMessageStruct( + commentAndBinaryAndSliceReceiver.address, + commentAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndBinaryAndSliceReceiver.address, + commentAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + commentAndBinaryAndSliceReceiver.address, + 2, + commentAndBinaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + + // stringAndBinaryAndSliceReceiver + // should accept the structs in binary + // should accept middle ones in fallback + // and the rest in the string receiver + await shouldAcceptMessageStruct( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptIncompleteOpCode( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyBody( + stringAndBinaryAndSliceReceiver.address, + stringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + stringAndBinaryAndSliceReceiver.address, + 4, + stringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback_string", + ); + + // emptyAndCommentAndStringAndBinaryReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndStringAndBinaryReceiver.address, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndStringAndBinaryReceiver.address, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndStringAndBinaryReceiver.address, + 2, + emptyAndCommentAndStringAndBinaryReceiver.getReceiver, + "external_empty", + ); + + // emptyAndCommentAndStringAndSliceReceiver + // structs in fallback + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndStringAndSliceReceiver.address, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndStringAndSliceReceiver.address, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + emptyAndCommentAndStringAndSliceReceiver.address, + 2, + emptyAndCommentAndStringAndSliceReceiver.getReceiver, + "external_empty", + ); + + // emptyAndCommentAndBinaryAndSliceReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndCommentAndBinaryAndSliceReceiver.address, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndCommentAndBinaryAndSliceReceiver.address, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndCommentAndBinaryAndSliceReceiver.address, + 2, + emptyAndCommentAndBinaryAndSliceReceiver.getReceiver, + "external_empty", + ); + + // emptyAndStringAndBinaryAndSliceReceiver + // structs in binary + // rest in empty + await shouldAcceptMessageStruct( + emptyAndStringAndBinaryAndSliceReceiver.address, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + emptyAndStringAndBinaryAndSliceReceiver.address, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + emptyAndStringAndBinaryAndSliceReceiver.address, + 2, + emptyAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_empty", + ); + + // commentAndStringAndBinaryAndSliceReceiver + // structs in binary + // middle ones in the fallback receiver, + // and the rest in the string receiver + await shouldAcceptMessageStruct( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptIncompleteOpCode( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptEmptyBody( + commentAndStringAndBinaryAndSliceReceiver.address, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback", + ); + await shouldAcceptFrom( + commentAndStringAndBinaryAndSliceReceiver.address, + 4, + commentAndStringAndBinaryAndSliceReceiver.getReceiver, + "external_fallback_string", + ); + + // allReceivers + // structs in binary + // the rest in the empty receiver + await shouldAcceptMessageStruct( + allReceivers.address, + allReceivers.getReceiver, + "external_binary", + ); + await shouldAcceptEmptyMessageStruct( + allReceivers.address, + allReceivers.getReceiver, + "external_binary_empty_message", + ); + await shouldAcceptFrom( + allReceivers.address, + 2, + allReceivers.getReceiver, + "external_empty", + ); + }); + + it("bounced receivers should process empty messages and empty strings correctly", async () => { + // A message struct with empty string inside + const emptyStringInMessageStruct = beginCell() + .storeUint(100, 32) + .storeStringRefTail("") + .endCell(); + // An empty message struct + const emptyMessageStruct = beginCell().storeUint(101, 32).endCell(); + // Message body with less than 32 bits in its opcode. + const lessThan32Bits = beginCell().storeUint(10, 30).endCell(); + // An actual empty message body + const emptyBody = new Cell(); + // Message body with integer of size exactly 32 bits but value 0. + const zeroOf32Bits = beginCell().storeUint(0, 32).endCell(); + // Empty string + const emptyString = beginCell() + .storeUint(0, 32) + .storeStringTail("") + .endCell(); + + const bodiesToTry = [ + emptyStringInMessageStruct, + emptyMessageStruct, + lessThan32Bits, + emptyBody, + zeroOf32Bits, + emptyString, + ]; + + // Some utility functions that carry out the actual tests and assertions + + async function shouldAcceptFrom( + from: number, + testedContract: Address, + testedContractSend: ( + sender: Sender, + args: { value: bigint }, + body: SendCellToAddress | "reset", + ) => Promise, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + for (const body of bodiesToTry.slice(from)) { + await shouldAcceptBody( + testedContract, + testedContractSend, + receiverGetter, + expectedReceiver, + body, + ); + } + } + + async function shouldAcceptMessageStruct( + testedContract: Address, + testedContractSend: ( + sender: Sender, + args: { value: bigint }, + body: SendCellToAddress | "reset", + ) => Promise, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + testedContractSend, + receiverGetter, + expectedReceiver, + emptyStringInMessageStruct, + ); + } + + async function shouldAcceptEmptyMessageStruct( + testedContract: Address, + testedContractSend: ( + sender: Sender, + args: { value: bigint }, + body: SendCellToAddress | "reset", + ) => Promise, + receiverGetter: () => Promise, + expectedReceiver: string, + ) { + await shouldAcceptBody( + testedContract, + testedContractSend, + receiverGetter, + expectedReceiver, + emptyMessageStruct, + ); + } + + async function shouldAcceptBody( + testedContract: Address, + testedContractSend: ( + sender: Sender, + args: { value: bigint }, + body: SendCellToAddress | "reset", + ) => Promise, + receiverGetter: () => Promise, + expectedReceiver: string, + body: Cell, + ) { + // Request testedContract to send a message to the contract that has no receivers. + // Such contract will reject all messages and bounce them back into the + // testedContract + const { transactions } = await testedContractSend( + treasure.getSender(), + { value: toNano("10") }, + { + $$type: "SendCellToAddress", + address: noReceivers.address, + body: body, + }, + ); + + expect(transactions).toHaveTransaction({ + from: noReceivers.address, + to: testedContract, + success: true, + }); + expect(await receiverGetter()).toBe(expectedReceiver); + + const resetResult = await testedContractSend( + treasure.getSender(), + { value: toNano("10") }, + "reset", + ); + + expect(resetResult.transactions).toHaveTransaction({ + from: treasure.address, + to: testedContract, + success: true, + }); + expect(await receiverGetter()).toBe("unknown"); + } + + // Tests start here + + // emptyBounced should ignore all bounced messages without errors. + // It succeeds its transaction for all cases but remains in "unknown" receiver + await shouldAcceptFrom( + 0, + emptyBounced.address, + emptyBounced.send, + emptyBounced.getReceiver, + "unknown", + ); + + // binaryBounced + // It will catch the bounced binary messages. + // The rest will ignore them without error, and remain in "unknown". + await shouldAcceptMessageStruct( + binaryBounced.address, + binaryBounced.send, + binaryBounced.getReceiver, + "bounced_binary", + ); + await shouldAcceptEmptyMessageStruct( + binaryBounced.address, + binaryBounced.send, + binaryBounced.getReceiver, + "bounced_binary_empty_message", + ); + await shouldAcceptFrom( + 2, + binaryBounced.address, + binaryBounced.send, + binaryBounced.getReceiver, + "unknown", + ); + + // sliceBounced + // It will catch all cases in the fallback bounced receiver + await shouldAcceptFrom( + 0, + sliceBounced.address, + sliceBounced.send, + sliceBounced.getReceiver, + "bounced_fallback", + ); + + // allBounced + // It will catch the bounced binary messages in the binary bounced receivers + // The rest in the fallback bounced receiver + await shouldAcceptMessageStruct( + allBounced.address, + allBounced.send, + allBounced.getReceiver, + "bounced_binary", + ); + await shouldAcceptEmptyMessageStruct( + allBounced.address, + allBounced.send, + allBounced.getReceiver, + "bounced_binary_empty_message", + ); + await shouldAcceptFrom( + 2, + allBounced.address, + allBounced.send, + allBounced.getReceiver, + "bounced_fallback", + ); + }); }); + +function getTransactionDescription( + tsx: SmartContractTransaction, +): TransactionDescriptionGeneric { + if (tsx.description.type === "generic") { + return tsx.description; + } + throw new Error("Expected generic transaction"); +}