Skip to content

Commit

Permalink
docs: contract parameters (#2071)
Browse files Browse the repository at this point in the history
  • Loading branch information
novusnota authored Feb 28, 2025
1 parent 563a164 commit 9991da4
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 317 deletions.
1 change: 1 addition & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Calling methods on `null` when `self` is of an optional type is now allowed: PR [#1567](https://github.com/tact-lang/tact/pull/1567)
- Constants and trait constants can now depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622)
- Support overriding constants and methods of the `BaseTrait` trait: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
- Introduced contract parameters as a replacement for the lazy initialization via `init()` function: PR [#1985](https://github.com/tact-lang/tact/pull/1985), PR [#2071](https://github.com/tact-lang/tact/pull/2071)
- [fix] Collisions in getter method ids are now handled and reported properly: PR [#875](https://github.com/tact-lang/tact/pull/875), PR [#1052](https://github.com/tact-lang/tact/pull/1052)
- [fix] The `as coins` map value serialization type is now handled correctly: PR [#987](https://github.com/tact-lang/tact/pull/987)
- [fix] Fixed type checking of `foreach` loops in trait methods: PR [#1017](https://github.com/tact-lang/tact/pull/1017)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/book/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ If set to `false{:json}`, enables saving the contract state at the end of a rece

`false{:json}` by default.

If set to `true{:json}`, enables generation of `lazy_deployment_completed()` getter.
If set to `true{:json}`, enables generation of `lazy_deployment_completed()` getter. Does nothing if [contract parameters](/book/contracts#parameters) are declared.

```json filename="tact.config.json" {8}
{
Expand Down
111 changes: 99 additions & 12 deletions docs/src/content/docs/book/contracts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Contracts
description: "Contracts in Tact are similar to classes in popular object-oriented languages, except that their instances are deployed on the blockchain and they can't be passed around like Structs and Messages"
---

import { Badge } from '@astrojs/starlight/components';

Contracts in Tact are similar to classes in popular object-oriented languages, except that their instances are deployed on the blockchain and they can't be passed around like [Structs and Messages](/book/structs-and-messages).

## Self-references {#self}
Expand All @@ -18,14 +20,24 @@ contract Example {
self.foo = 42; // <- referencing variable foo through self.
}
}
contract ExampleParams(
// persistent state variables
foo: Int,
) {
receive() {
self.foo = 42; // <- referencing variable foo through self.
}
}
```

## Structure

Each contract can contain:
Each contract can be defined with or contain:

* [Inherited traits](#traits)
* [Supported interfaces](#interfaces)
* [Contract parameters](#parameters)
* [Persistent state variables](#variables)
* [Constructor function `init(){:tact}`](#init-function)
* [Contract constants](#constants)
Expand Down Expand Up @@ -53,16 +65,22 @@ contract Plural with
{}
```

As [traits][trait] are not allowed to have [`init(){:tact}` function](#init-function), a contract inheriting a trait with any [persistent state variables](#variables) declared must initialize them by providing its own [`init(){:tact}` function](#init-function).
As [traits][trait] are not allowed to have [`init(){:tact}` function](#init-function), a contract inheriting a trait with any [persistent state variables](#variables) declared must either provide its own [`init(){:tact}` function](#init-function) or declare [contract parameters](#parameters).

```tact
trait Supe { omelander: Bool }
contract Vot with Supe {
init() {
self.omelander = true;
}
// This contract will perform lazy initialization,
// setting its data on-chain after the initial deployment
contract LazyVot with Supe {
init() { self.omelander = false }
}
// This contract will directly set its values with the
// initial data received from the deployment message
contract Vot(
omelander: Bool,
) with Supe {}
```

If declared or defined in a trait, internal functions and constants can be marked as [virtual or abstract](/book/functions#virtual-and-abstract-functions) and overridden in contracts inheriting from the trait.
Expand Down Expand Up @@ -135,21 +153,65 @@ In addition to that, [`tact.config.json`](/book/config) may still be used in [Bl

:::

### Contract parameters {#parameters}

<Badge text="Available since Tact 1.6 (not released yet)" variant="tip" size="medium"/><p/>

Most of the contracts do not need any on-chain deployment logic that runs just once after the contract's deployment is completed. As such, the costly lazy deployment strategy provided by the [`init(){:tact}`](#init-function) is generally discouraged in favor of simple, direct deployments with [initial code and initial data](/book/expressions#initof).

To declare [persistent state variables](#variables) for their values to be set during such deployments, use the following _contract parameters_ syntax:

```tact
contract ContractParameters(
// persistent state variables declared via the contract parameters syntax
val: Int, // Int
val32: Int as uint32, // Int serialized to an 32-bit unsigned
mapVal: map<Int, Int>, // Int keys to Int values
optVal: Int?, // Int or null
) {
// ...
}
```

State variables declared in this way have **no** default value. Instead, they get their initial data only when the contract is deployed, which is much cheaper compared to deployments with an extra initialization step in the [`init(){:tact}`](#init-function).

The use of contract parameters syntax conflicts with using an [`init(){:tact}`](#init-function) function or declaring persistent state variables via the [contract fields syntax](#variables). Whenever possible, prefer using contract parameters unless you need some on-chain deployment logic that needs to be run only once, i.e. via [`init(){:tact}`](#init-function) function.

```tact
contract ParamParamParam(
param1: Int,
param2: Int,
) {
// COMPILATION ERROR! init() cannot be used along with contract parameters
init(param1: Int, param2: Int) {}
}
```

:::note

To obtain initial state of the target contract in [internal functions](#internal-functions), [receivers](#receiver-functions) or [getters](#getter-functions) use the [`initOf{:tact}`](/book/expressions#initof) expression.

To only get the contract's code, use the [`codeOf{:tact}`](/book/expressions#codeof) expression.

:::

### Persistent state variables {#variables}

Contracts can define state variables that persist between contract calls, and thus commonly referred to as _storage_ variables. Contracts in TON [pay rent](https://docs.ton.org/develop/smart-contracts/fees#storage-fee) in proportion to the amount of persistent space they consume, so [compact representations via serialization](/book/integers#serialization) are encouraged.

The storage variables can be defined by using the [contract parameters](#parameters) or by using the following _contract fields_ syntax:

```tact
contract Example {
// persistent state variables
contract ContractFields {
// persistent state variables declared via the contract fields syntax
val: Int; // Int
val32: Int as uint32; // Int serialized to an 32-bit unsigned
mapVal: map<Int, Int>; // Int keys to Int values
optVal: Int?; // Int or null
}
```

State variables must have a default value or be initialized in the [`init(){:tact}`](#init-function) function that runs once on deployment of the contract. The only exception are persistent state variables of type [`map<K, V>{:tact}`](/book/maps), since they are initialized empty by default.
State variables defined via the contract fields syntax must have a default value or be initialized in the [`init(){:tact}`](#init-function) function that runs once on deployment of the contract. The only exception are persistent state variables of type [`map<K, V>{:tact}`](/book/maps), since they are initialized empty by default.

The default value of state variables is assigned before any values could be assigned in the [`init(){:tact}`](#init-function) function.

Expand Down Expand Up @@ -203,6 +265,19 @@ contract Zero {
}
```

The above contract fields syntax for declaring persistent state variables conflicts with the use of [contract parameters](#parameters). Use one or the other, but not both.

```tact
contract ParamParamParam(
param1: Int,
param2: Int,
) {
// COMPILATION ERROR! cannot use contract fields along with contract parameters
field1: Int;
field2: Int;
}
```

:::note

Tact supports local, non-persistent-state variables too, see: [Variable declaration](/book/statements#let).
Expand All @@ -215,7 +290,7 @@ Unlike [variables](#variables), constants cannot change. Their values are calcul

There isn't much difference between constants defined outside of a contract (global constants) and inside the contract (contract constants). Those defined outside can be used by other contracts in your project.

Constant initializations must be relatively simple and only rely on values known during compilation. If you add two numbers for example, the compiler will calculate the result during build and put the result in your compiled code.
Constant initialization must be relatively simple and only rely on values known during compilation. If you add two numbers for example, the compiler will calculate the result during build and put the result in your compiled code.

You can read constants both in [receivers](#receiver-functions) and in [getters](#getter-functions).

Expand Down Expand Up @@ -246,7 +321,7 @@ Read more about constants on their dedicated page: [Constants](/book/constants).

### Constructor function `init()` {#init-function}

On deployment of the contract, the constructor function `init(){:tact}` is run.
On deployment of the contract, the constructor function `init(){:tact}` is run. Unlike [contract parameters](#parameters), it performs a delayed initialization of the contract data, setting the values of persistent state variables on-chain.

If a contract has any [persistent state variables](#variables) without default values specified, it must initialize them in this function.

Expand All @@ -265,7 +340,7 @@ contract Example {
}
```

If a contract doesn't have any persistent state variables, or they all have their default value specified, it may omit the `init(){:tact}` function declaration altogether. That's because unless explicitly declared, the empty `init(){:tact}` function is present by default in all contracts.
If a contract doesn't have any persistent state variables, or they all have their default value specified, it may omit the `init(){:tact}` function declaration altogether. That's because unless explicitly declared, the empty `init(){:tact}` function is present by default in all contracts that do not declare the [contract parameters](#parameters).

The following is an example of a valid empty contract:

Expand All @@ -286,6 +361,18 @@ contract TheySeeMeTrailing {
}
```

Usage of the `init(){:tact}` function conflicts with the use of [contract parameters](#parameters). Use one or the other, but not both.

```tact
contract ParamParamParam(
param1: Int,
param2: Int,
) {
// COMPILATION ERROR! init() cannot be used along with contract parameters
init(param1: Int, param2: Int) {}
}
```

:::note

To obtain initial state of the target contract in [internal functions](#internal-functions), [receivers](#receiver-functions) or [getters](#getter-functions) use the [`initOf{:tact}`](/book/expressions#initof) expression.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/book/debug.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Then, a `beforeEach(){:tact}` [Jest][jest] function is called — it specifies a

It is strongly advised not to modify the contents of `beforeEach(){:tact}`, unless you really need some specific behavior for each test closure or parameters of your [`init(){:tact}`](/book/contracts#init-function) function have changed.

Additionally, you may need to edit `beforeEach(){:tact}` contents if you've changed the [contract parameters](/book/contracts#parameters).

:::

Finally, each test closure is described with a call to `it(){:tact}` [Jest][jest] function — that's where tests are actually written.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/book/expressions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ The `StateInit{:tact}` is a [Struct][s] consisting of the following fields:
Field | Type | Description
:----- | :-------------------- | :----------
`code` | [`Cell{:tact}`][cell] | initial code of the [contract](/book/contracts) (compiled bitcode)
`data` | [`Cell{:tact}`][cell] | initial data of the [contract](/book/contracts) (parameters of [`init(){:tact}`](/book/contracts#init-function) function)
`data` | [`Cell{:tact}`][cell] | initial data of the [contract](/book/contracts) (parameters of [`init(){:tact}`](/book/contracts#init-function) function or [contract parameters](/book/contracts#parameters))

:::note

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/book/integers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Therefore, the amount of $1.25$ Toncoin, which can be represented in Tact as [`t

## Serialization

When encoding `Int{:tact}` values to persistent state (fields of [contracts](/book/contracts) and [traits](/book/types#traits)), it's usually better to use smaller representations than $257$-bits to reduce [storage costs](https://docs.ton.org/develop/smart-contracts/fees#storage-fee). Usage of such representations is also called "serialization" due to them representing the native [TL-B][tlb] types which TON Blockchain operates on.
When encoding `Int{:tact}` values to persistent state (fields or parameters of [contracts](/book/contracts) and fields of [traits](/book/types#traits)), it's usually better to use smaller representations than $257$-bits to reduce [storage costs](https://docs.ton.org/develop/smart-contracts/fees#storage-fee). Usage of such representations is also called "serialization" due to them representing the native [TL-B][tlb] types which TON Blockchain operates on.

The persistent state size is specified in every declaration of a state variable after the `as{:tact}` keyword:

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/book/send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Field | Type | Description
`mode` | [`Int{:tact}`][int] | An 8-bit value that configures how to send a message, defaults to $0$. See: [Message `mode`](/book/message-mode).
`body` | [`Cell?{:tact}`][cell] | [Optional][opt] message body as a [`Cell{:tact}`][cell].
`code` | [`Cell?{:tact}`][cell] | [Optional][opt] initial code of the contract (compiled bitcode).
`data` | [`Cell?{:tact}`][cell] | [Optional][opt] initial data of the contract (arguments of [`init(){:tact}` function](/book/contracts#init-function)).
`data` | [`Cell?{:tact}`][cell] | [Optional][opt] initial data of the contract (arguments of [`init(){:tact}` function](/book/contracts#init-function) or values of [contract parameters](/book/contracts#parameters)).
`value` | [`Int{:tact}`][int] | The amount of [nanoToncoins][nano] you want to send with the message. This value is used to cover [forward fees][fwdfee], unless the optional flag [`SendPayGasSeparately{:tact}`](/book/message-mode#optional-flags) is used.
`to` | [`Address{:tact}`][p] | Recipient internal [`Address{:tact}`][p] on TON Blockchain.
`bounce` | [`Bool{:tact}`][p] | When set to `true` (default) message bounces back to the sender if the recipient contract doesn't exist or wasn't able to process the message.
Expand Down
11 changes: 10 additions & 1 deletion docs/src/content/docs/book/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,18 @@ contract Treasure with Ownable {
// Persistent state variable, which MUST be defined in the contract
owner: Address;
// Constructor function init(), where all the variables are initialized
// Constructor function init(), where all the variables are initialized on-chain
init(owner: Address) {
self.owner = owner;
}
}
```

Alternatively, a contract may declare use the [contract parameters syntax](/book/contracts#parameters), in which case they must list all the persistent state variables inherited from all of its traits:

```tact
contract Treasure(
// Persistent state variable, to be defined at deployment
owner: Address,
) with Ownable {}
```
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type Options = {
readonly optimizations?: OptimizationOptions;
/**
* If set to true, enables generation of `lazy_deployment_completed()` getter.
* Does nothing if contract parameters are declared.
*/
readonly enableLazyDeploymentCompletedGetter?: boolean;
};
Expand Down
1 change: 1 addition & 0 deletions src/config/config.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const optionsSchema: z.ZodType<C.Options> = z.object({
optimizations: optimizationOptionsSchema.optional(),
/**
* If set to true, enables generation of `lazy_deployment_completed()` getter.
* Does nothing if contract parameters are declared.
*/
enableLazyDeploymentCompletedGetter: z.boolean().optional(),
});
Expand Down
2 changes: 1 addition & 1 deletion src/config/configSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"enableLazyDeploymentCompletedGetter": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables generation of `lazy_deployment_completed()` getter."
"description": "False by default. If set to true, enables generation of `lazy_deployment_completed()` getter. Does nothing if contract parameters are declared."
}
}
},
Expand Down
Loading

0 comments on commit 9991da4

Please sign in to comment.