Skip to content

Commit 79ff790

Browse files
author
Anton Trunov
committed
feat: fromCell and fromSlice methods on contract types
1 parent 0cdc254 commit 79ff790

11 files changed

+502
-4
lines changed

dev-docs/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Language features
11+
12+
- Support `fromCell()` and `fromSlice()` methods on contract types: PR [#3303](https://github.com/tact-lang/tact/pull/3303)
13+
14+
### Release contributors
15+
16+
- [Anton Trunov](https://github.com/anton-trunov)
17+
1018
## [1.6.12] - 2025-05-27
1119

1220
### Language features

src/abi/contracts.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ops } from "@/generator/writers/ops";
33
import { writeExpression } from "@/generator/writers/writeExpression";
44
import { throwCompilationError } from "@/error/errors";
55
import type { AbiFunction } from "@/abi/AbiFunction";
6-
import { getTypeOrUndefined } from "@/types/resolveDescriptors";
6+
import { getType, getTypeOrUndefined } from "@/types/resolveDescriptors";
77
import { getExpType } from "@/types/resolveExpression";
88
import type { WriterContext } from "@/generator/Writer";
99

@@ -70,6 +70,128 @@ export const ContractFunctions: Map<string, AbiFunction> = new Map([
7070
},
7171
},
7272
],
73+
[
74+
"fromCell",
75+
{
76+
name: "fromCell",
77+
isStatic: true,
78+
resolve: (ctx, args, ref) => {
79+
if (args.length !== 2) {
80+
throwCompilationError(
81+
"fromCell() expects one argument",
82+
ref,
83+
);
84+
}
85+
const contract = args[0]!;
86+
const cell = args[1]!;
87+
if (contract.kind !== "ref") {
88+
throwCompilationError(
89+
"fromCell() is implemented only for struct/contract types",
90+
ref,
91+
);
92+
}
93+
const contractTy = getType(ctx, contract.name);
94+
if (contractTy.kind !== "contract") {
95+
throwCompilationError(
96+
"fromCell() is implemented only for struct/contract types",
97+
ref,
98+
);
99+
}
100+
if (cell.kind !== "ref" || cell.name !== "Cell") {
101+
throwCompilationError(
102+
"fromCell() expects a Cell as an argument",
103+
ref,
104+
);
105+
}
106+
return { kind: "ref", name: contract.name, optional: false };
107+
},
108+
generate: (ctx, args, resolved, ref) => {
109+
if (resolved.length !== 2) {
110+
throwCompilationError(
111+
"fromCell() expects one argument",
112+
ref,
113+
);
114+
}
115+
const contract = args[0]!;
116+
const cell = args[1]!;
117+
if (contract.kind !== "ref") {
118+
throwCompilationError(
119+
"fromCell() is implemented only for struct/contract types",
120+
ref,
121+
);
122+
}
123+
if (cell.kind !== "ref" || cell.name !== "Cell") {
124+
throwCompilationError(
125+
"fromCell() expects a Cell as an argument",
126+
ref,
127+
);
128+
}
129+
const skip = skipLazyBit(contract.name, ctx);
130+
return `${ops.readerNonModifying(contract.name, ctx)}(${writeExpression(resolved[1]!, ctx)}.begin_parse()${skip})`;
131+
},
132+
},
133+
],
134+
[
135+
"fromSlice",
136+
{
137+
name: "fromSlice",
138+
isStatic: true,
139+
resolve: (ctx, args, ref) => {
140+
if (args.length !== 2) {
141+
throwCompilationError(
142+
"fromSlice() expects one argument",
143+
ref,
144+
);
145+
}
146+
const contract = args[0]!;
147+
const slice = args[1]!;
148+
if (contract.kind !== "ref") {
149+
throwCompilationError(
150+
"fromSlice() is implemented only for struct/contract types",
151+
ref,
152+
);
153+
}
154+
const contractTy = getType(ctx, contract.name);
155+
if (contractTy.kind !== "contract") {
156+
throwCompilationError(
157+
"fromSlice() is implemented only for struct/contract types",
158+
ref,
159+
);
160+
}
161+
if (slice.kind !== "ref" || slice.name !== "Slice") {
162+
throwCompilationError(
163+
"fromSlice() expects a Slice as an argument",
164+
ref,
165+
);
166+
}
167+
return { kind: "ref", name: contract.name, optional: false };
168+
},
169+
generate: (ctx, args, resolved, ref) => {
170+
if (resolved.length !== 2) {
171+
throwCompilationError(
172+
"fromSlice() expects one argument",
173+
ref,
174+
);
175+
}
176+
const contract = args[0]!;
177+
const slice = args[1]!;
178+
if (contract.kind !== "ref") {
179+
throwCompilationError(
180+
"fromSlice() is implemented only for struct/contract types",
181+
ref,
182+
);
183+
}
184+
if (slice.kind !== "ref" || slice.name !== "Slice") {
185+
throwCompilationError(
186+
"fromSlice() expects a Slice as an argument",
187+
ref,
188+
);
189+
}
190+
const skip = skipLazyBit(contract.name, ctx);
191+
return `${ops.readerNonModifying(contract.name, ctx)}(${writeExpression(resolved[1]!, ctx)}${skip})`;
192+
},
193+
},
194+
],
73195
]);
74196

75197
function lazyBitBuilder(
@@ -90,3 +212,8 @@ function lazyBitBuilder(
90212

91213
return `begin_cell()`;
92214
}
215+
216+
function skipLazyBit(contractName: string, ctx: WriterContext): string {
217+
const ty = getType(ctx.ctx, contractName);
218+
return ty.init?.kind === "init-function" ? ".skip_bits(1)" : "";
219+
}

src/abi/struct.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,14 @@ export const StructFunctions: Map<string, AbiFunction> = new Map([
226226
const arg1 = args[1]!;
227227
if (arg0.kind !== "ref") {
228228
throwCompilationError(
229-
"fromSlice() is implemented only for struct types",
229+
"fromSlice() is implemented only for struct/contract types",
230230
ref,
231231
);
232232
}
233233
const tp = getType(ctx, arg0.name);
234234
if (tp.kind !== "struct") {
235235
throwCompilationError(
236-
"fromSlice() is implemented only for struct types",
236+
"fromSlice() is implemented only for struct/contract types",
237237
ref,
238238
);
239239
}
@@ -256,7 +256,7 @@ export const StructFunctions: Map<string, AbiFunction> = new Map([
256256
const arg1 = args[1]!;
257257
if (arg0.kind !== "ref") {
258258
throwCompilationError(
259-
"fromSlice() is implemented only for struct types",
259+
"fromSlice() is implemented only for struct/contract types",
260260
ref,
261261
);
262262
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { beginCell, toNano } from "@ton/core";
2+
import { Blockchain } from "@ton/sandbox";
3+
import { Test } from "./output/from-cell-contract-parameters_Test";
4+
import "@ton/test-utils";
5+
import { cached } from "@/test/utils/cache-state";
6+
7+
const deployValue = toNano("1");
8+
9+
const setup = async () => {
10+
const blockchain = await Blockchain.create();
11+
blockchain.verbosity.print = false;
12+
13+
const treasury = await blockchain.treasury("treasury");
14+
const contract = blockchain.openContract(await Test.fromInit(0n, 1n));
15+
16+
const deployResult = await contract.send(
17+
treasury.getSender(),
18+
{ value: deployValue },
19+
null,
20+
);
21+
expect(deployResult.transactions).toHaveTransaction({
22+
from: treasury.address,
23+
to: contract.address,
24+
success: true,
25+
deploy: true,
26+
});
27+
28+
return {
29+
blockchain,
30+
treasury,
31+
contract,
32+
};
33+
};
34+
35+
describe("fromCell for contract with parameters", () => {
36+
const state = cached(setup);
37+
38+
it("should correctly deserialize", async () => {
39+
const { contract, treasury } = await state.get();
40+
41+
await contract.send(
42+
treasury.getSender(),
43+
{
44+
value: toNano("1"),
45+
},
46+
{
47+
$$type: "NewContractData",
48+
cell: beginCell().storeUint(1, 32).storeUint(2, 32).endCell(),
49+
},
50+
);
51+
expect(await contract.getState()).toMatchObject({
52+
$$type: "Test$Data",
53+
x: 1n,
54+
y: 2n,
55+
});
56+
expect(await contract.getInverseLaw()).toEqual(true);
57+
});
58+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// Test.fromCell(self.toCell()) = self
3+
//
4+
5+
message NewContractData {
6+
cell: Cell;
7+
}
8+
9+
contract Test(
10+
x: Int as uint32,
11+
y: Int as uint32,
12+
) {
13+
14+
receive() { }
15+
16+
receive(msg: NewContractData) {
17+
let c = Test.fromCell(msg.cell);
18+
self.x = c.x;
19+
self.y = c.y;
20+
}
21+
22+
get fun state(): Test {
23+
return self;
24+
}
25+
get fun inverseLaw(): Bool {
26+
return (Test.fromCell(self.toCell())).toCell() == self.toCell();
27+
}
28+
}
29+
30+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { beginCell, toNano } from "@ton/core";
2+
import { Blockchain } from "@ton/sandbox";
3+
import { Test } from "./output/from-cell-fields_Test";
4+
import "@ton/test-utils";
5+
import { cached } from "@/test/utils/cache-state";
6+
7+
const deployValue = toNano("1");
8+
9+
const setup = async () => {
10+
const blockchain = await Blockchain.create();
11+
blockchain.verbosity.print = false;
12+
13+
const treasury = await blockchain.treasury("treasury");
14+
const contract = blockchain.openContract(await Test.fromInit());
15+
16+
const deployResult = await contract.send(
17+
treasury.getSender(),
18+
{ value: deployValue },
19+
null,
20+
);
21+
expect(deployResult.transactions).toHaveTransaction({
22+
from: treasury.address,
23+
to: contract.address,
24+
success: true,
25+
deploy: true,
26+
});
27+
28+
return {
29+
blockchain,
30+
treasury,
31+
contract,
32+
};
33+
};
34+
35+
describe("fromCell for contract with fields", () => {
36+
const state = cached(setup);
37+
38+
it("should correctly deserialize", async () => {
39+
const { contract, treasury } = await state.get();
40+
41+
await contract.send(
42+
treasury.getSender(),
43+
{
44+
value: toNano("1"),
45+
},
46+
{
47+
$$type: "NewContractData",
48+
cell: beginCell()
49+
.storeBit(1)
50+
.storeUint(1, 32)
51+
.storeUint(2, 32)
52+
.endCell(),
53+
},
54+
);
55+
expect(await contract.getState()).toMatchObject({
56+
$$type: "Test$Data",
57+
x: 1n,
58+
y: 2n,
59+
});
60+
expect(await contract.getInverseLaw()).toEqual(true);
61+
});
62+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Test.fromCell(self.toCell()) = self
3+
//
4+
5+
message NewContractData {
6+
cell: Cell;
7+
}
8+
9+
contract Test {
10+
x: Int as uint32 = 0;
11+
y: Int as uint32 = 1;
12+
13+
receive() { }
14+
15+
receive(msg: NewContractData) {
16+
let c = Test.fromCell(msg.cell);
17+
self.x = c.x;
18+
self.y = c.y;
19+
}
20+
21+
get fun state(): Test {
22+
return self;
23+
}
24+
get fun inverseLaw(): Bool {
25+
return (Test.fromCell(self.toCell())).toCell() == self.toCell();
26+
}
27+
}
28+
29+

0 commit comments

Comments
 (0)