Structs and Messages
Tact supports a number of primitive data types that are tailored for smart contract use. However, using individual means of storage often becomes cumbersome, so there are Structs and Messages which allow combining types together.
After a successful compilation, Tact produces a compilation report, which features all the declared Structs and Messages, including those from the Core standard library. See the Structures section of the compilation report for details.
Structs
Structs can define complex data types that contain multiple fields of different types. They can also be nested.
struct Point { x: Int as int64; y: Int as int64;}
struct Line { start: Point; end: Point;}
Structs can also contain default fields and define fields of optional types. This can be useful if you have a lot of fields, but don’t want to keep having to specify common values for them in new instances.
struct Params { name: String = "Satoshi"; // default value
age: Int?; // field with an optional type Int? // and default value of null
point: Point; // nested Structs}
Structs are also useful as return values from getters or other internal functions. They effectively allow a single getter to return multiple return values.
contract StructsShowcase { params: Params; // Struct as a contract's persistent state variable
init() { self.params = Params{ point: Point{ x: 4, y: 2, }, }; }
get fun params(): Params { return self.params; }}
Note, that the last semicolon ;
in Struct declaration is optional and may be omitted:
struct Mad { ness: Bool }
struct MoviesToWatch { wolverine: String; redFunnyGuy: String}
The order of fields matters, as it corresponds to the resulting memory layout in TL-B schemas. However, unlike some languages with manual memory management, Tact does not have any padding between fields.
Messages
Messages can hold Structs in them:
struct Point { x: Int; y: Int;}
message Add { point: Point; // holds a struct Point}
Message opcodes
Messages are almost the same thing as Structs with the only difference that Messages have a 32-bit integer header in their serialization containing their unique numeric id, commonly referred to as an opcode (operation code). This allows Messages to be used with receivers since the contract can tell different types of messages apart based on this id.
Tact automatically generates those unique ids (opcodes) for every received Message, which can be observed in the Structures section of the compilation report.
Besides, opcodes can be overwritten manually:
// This Message overwrites its unique id (opcode) with 0x7362d09cmessage(0x7362d09c) TokenNotification { forwardPayload: Slice as remaining;}
This is useful for cases where you want to handle certain opcodes of a given smart contract, such as Jetton standard. The short-list of opcodes this contract is able to process is given here in FunC. They serve as an interface to the smart contract.
Available since Tact 1.6 (not released yet) A message opcode can be any compile-time expression that evaluates to a positive -bit integer, so the following is also valid:
// This Message overwrites its unique id (opcode) with 898001897,// which is the evaluated integer value of the specified compile-time expressionmessage((crc32("Tact") + 42) & 0xFFFF_FFFF) MsgWithExprOpcode { field: Int as uint4;}
Operations
Instantiate
Creation of Struct and Message instances resembles function calls, but instead of paretheses ()
one needs to specify arguments in braces {}
(curly brackets):
struct StA { field1: Int; field2: Int;}
message MsgB { field1: String; field2: String;}
fun example() { // Instance of a Struct StA StA{ field1: 42, field2: 68 + 1, // trailing comma is allowed };
// Instance of a Message MsgB MsgB{ field1: "May the 4th", field2: "be with you!", // trailing comma is allowed };}
When the name of a variable or constant assigned to a field coincides with the name of such field, Tact provides a handy syntactic shortcut sometimes called field punning. With it, you don’t have to type more than it’s necessary:
struct PopQuiz { vogonsCount: Int; nicestNumber: Int;}
fun example() { // Let's introduce a couple of variables let vogonsCount: Int = 42; let nicestNumber: Int = 68 + 1;
// You may instantiate the Struct as usual and assign variables to fields, // but that is a bit repetitive and tedious at times PopQuiz{ vogonsCount: vogonsCount, nicestNumber: nicestNumber };
// Let's use field punning and type less, // because our variable names happen to be the same as field names PopQuiz{ vogonsCount, nicestNumber, // trailing comma is allowed here too! };}
Convert to a Cell
, .toCell()
It’s possible to convert an arbitrary Struct or Message to the Cell
type by using the .toCell()
extension function:
struct Big { f1: Int; f2: Int; f3: Int; f4: Int; f5: Int; f6: Int;}
fun conversionFun() { dump(Big{ f1: 10000000000, f2: 10000000000, f3: 10000000000, f4: 10000000000, f5: 10000000000, f6: 10000000000, }.toCell()); // x{...cell with references...}}
Obtain from a Cell
or Slice
, .fromCell()
and .fromSlice()
Instead of manually parsing a Cell
or Slice
via a series of relevant .loadSomething()
function calls, one can use .fromCell()
and .fromSlice()
extension functions for converting the provided Cell
or Slice
into the needed Struct or Message.
Those extension functions only attempt to parse a Cell
or Slice
according to the structure of your Struct or Message. In case layouts don’t match, various exceptions may be thrown — make sure to wrap your code in try...catch
blocks to prevent unexpected results.
struct Fizz { foo: Int }message(100) Buzz { bar: Int }
fun constructThenParse() { let fizzCell = Fizz{foo: 42}.toCell(); let buzzCell = Buzz{bar: 27}.toCell();
let parsedFizz: Fizz = Fizz.fromCell(fizzCell); let parsedBuzz: Buzz = Buzz.fromCell(buzzCell);}
Conversion laws
Whenever one converts between Cell
/Slice
and Struct/Message via .toCell()
and .fromCell()
functions, the following laws hold:
- For any instance of type Struct/Message, calling
.toCell()
on it, then applyingStruct.fromCell()
(orMessage.fromCell()
) to the result gives back the copy of the original instance:
struct ArbitraryStruct {}message(0x2A) ArbitraryMessage {}
fun lawOne() { let structInst = ArbitraryStruct{}; let messageInst = ArbitraryMessage{};
ArbitraryStruct.fromCell(structInst.toCell()); // = structInst ArbitraryMessage.fromCell(messageInst.toCell()); // = messageInst
// Same goes for Slices, with .toCell().asSlice() and .fromSlice()
ArbitraryStruct.fromSlice(structInst.toCell().asSlice()); // = structInst ArbitraryMessage.fromSlice(messageInst.toCell().asSlice()); // = messageInst}
- For any
Cell
with the same TL-B layout as a given Struct/Message, callingStruct.fromCell()
(orMessage.fromCell()
) on it, and then converting the result to aCell
via.toCell()
would give the copy of the originalCell
:
struct ArbitraryStruct { val: Int as uint32 }message(0x2A) ArbitraryMessage {}
fun lawTwo() { // Using 32 bits to store 42 just so this cellInst can be // re-used for working with both ArbitraryStruct and ArbitraryMessage let cellInst = beginCell().storeUint(42, 32).endCell();
ArbitraryStruct.fromCell(cellInst).toCell(); // = cellInst ArbitraryMessage.fromCell(cellInst).toCell(); // = cellInst
// Same goes for Slices, with .fromSlice() and .toCell().asSlice() let sliceInst = cellInst.asSlice();
ArbitraryStruct.fromSlice(sliceInst).toCell().asSlice(); // = sliceInst ArbitraryMessage.fromSlice(sliceInst).toCell().asSlice(); // = sliceInst}