From 30f7be2e653f4b2117714602add3a865ed9ef44e Mon Sep 17 00:00:00 2001 From: Daniil Sedov Date: Fri, 12 Jul 2024 21:39:21 +0300 Subject: [PATCH] feat/fix(ts): split serializers for `Cell`, `Slice` and `Builder` (#562) --- CHANGELOG.md | 1 + examples/wallet.spec.ts | 5 +- src/bindings/typescript/serializers.ts | 80 +++++++++++++------ .../contracts/serialization-3.tact | 2 +- src/test/e2e-emulated/dns.spec.ts | 30 +++---- src/test/e2e-emulated/intrinsics.spec.ts | 2 +- .../e2e-emulated/local-type-inference.spec.ts | 2 +- src/test/e2e-emulated/masterchain.spec.ts | 10 ++- src/test/e2e-emulated/math.spec.ts | 6 +- src/test/e2e-emulated/serialization.spec.ts | 27 ++++++- src/test/e2e-emulated/stdlib.spec.ts | 3 +- src/test/e2e-emulated/strings.spec.ts | 11 +-- src/test/e2e-emulated/structs.spec.ts | 20 +++-- 13 files changed, 135 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d12343e2..7616c2456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `[DEBUG]` prefix was removed from debug prints because a similar prefix was already present: PR [#506](https://github.com/tact-lang/tact/pull/506) - File paths in debug prints always use POSIX file paths (even on Windows): PR [#523](https://github.com/tact-lang/tact/pull/523) - The IPFS ABI and supported interfaces getters are not generated by default; to generate those, set to `true` the two newly introduced per-project options in `tact.config.json`: `ipfsAbiGetter` and `interfacesGetter`: PR [#534](https://github.com/tact-lang/tact/pull/534) +- Values of `Slice` and `Builder` types are not converted to `Cell` in Typescript bindings anymore: PR [#562](https://github.com/tact-lang/tact/pull/562) - Debug prints now include line content for better debugging experience: PR [#563](https://github.com/tact-lang/tact/pull/563) ### Fixed diff --git a/examples/wallet.spec.ts b/examples/wallet.spec.ts index c5bb2ce50..b6409a463 100644 --- a/examples/wallet.spec.ts +++ b/examples/wallet.spec.ts @@ -43,7 +43,10 @@ describe("wallet", () => { { $$type: "TransferMessage", transfer, - signature: beginCell().storeBuffer(signature).endCell(), + signature: beginCell() + .storeBuffer(signature) + .endCell() + .asSlice(), }, ); await system.run(); diff --git a/src/bindings/typescript/serializers.ts b/src/bindings/typescript/serializers.ts index 595f1c306..ccad9b03a 100644 --- a/src/bindings/typescript/serializers.ts +++ b/src/bindings/typescript/serializers.ts @@ -279,49 +279,77 @@ const addressSerializer: Serializer<{ optional: boolean }> = { }, }; +function getCellLikeTsType(v: { + kind: "cell" | "slice" | "builder"; + optional?: boolean; +}) { + return v.kind == "cell" ? "Cell" : v.kind == "slice" ? "Slice" : "Builder"; +} + +function getCellLikeTsAsMethod(v: { + kind: "cell" | "slice" | "builder"; + optional?: boolean; +}) { + if (v.optional) { + return `?.as${getCellLikeTsType(v)}() ?? null`; + } else { + return `.as${getCellLikeTsType(v)}()`; + } +} + const cellSerializer: Serializer<{ kind: "cell" | "slice" | "builder"; optional: boolean; }> = { tsType(v) { if (v.optional) { - return "Cell | null"; + return `${getCellLikeTsType(v)} | null`; } else { - return "Cell"; + return getCellLikeTsType(v); } }, tsLoad(v, slice, field, w) { if (v.optional) { w.append( - `let ${field} = ${slice}.loadBit() ? ${slice}.loadRef() : null;`, + `let ${field} = ${slice}.loadBit() ? ${slice}.loadRef()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""} : null;`, ); } else { - w.append(`let ${field} = ${slice}.loadRef();`); + w.append( + `let ${field} = ${slice}.loadRef()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + ); } }, tsLoadTuple(v, reader, field, w) { if (v.optional) { - w.append(`let ${field} = ${reader}.readCellOpt();`); + w.append( + `let ${field} = ${reader}.readCellOpt()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + ); } else { - w.append(`let ${field} = ${reader}.readCell();`); + w.append( + `let ${field} = ${reader}.readCell()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + ); } }, tsStore(v, builder, field, w) { if (v.optional) { w.append( - `if (${field} !== null && ${field} !== undefined) { ${builder}.storeBit(true).storeRef(${field}); } else { ${builder}.storeBit(false); }`, + `if (${field} !== null && ${field} !== undefined) { ${builder}.storeBit(true).storeRef(${field}${v.kind !== "cell" ? ".asCell()" : ""}); } else { ${builder}.storeBit(false); }`, ); } else { - w.append(`${builder}.storeRef(${field});`); + w.append( + `${builder}.storeRef(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + ); } }, tsStoreTuple(v, to, field, w) { - if (v.kind === "cell") { - w.append(`${to}.writeCell(${field});`); - } else if (v.kind === "slice") { - w.append(`${to}.writeSlice(${field});`); + if (v.optional) { + w.append( + `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? "?.asCell()" : ""});`, + ); } else { - w.append(`${to}.writeBuilder(${field});`); + w.append( + `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + ); } }, abiMatcher(src) { @@ -349,26 +377,28 @@ const cellSerializer: Serializer<{ const remainderSerializer: Serializer<{ kind: "cell" | "slice" | "builder" }> = { - tsType(_v) { - return "Cell"; + tsType(v) { + return getCellLikeTsType(v); }, tsLoad(v, slice, field, w) { - w.append(`let ${field} = ${slice}.asCell();`); + w.append( + `let ${field} = ${slice}${v.kind !== "slice" ? getCellLikeTsAsMethod(v) : ""};`, + ); }, tsLoadTuple(v, reader, field, w) { - w.append(`let ${field} = ${reader}.readCell();`); + w.append( + `let ${field} = ${reader}.readCell()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + ); }, tsStore(v, builder, field, w) { - w.append(`${builder}.storeBuilder(${field}.asBuilder());`); + w.append( + `${builder}.storeBuilder(${field}${v.kind !== "builder" ? ".asBuilder()" : ""});`, + ); }, tsStoreTuple(v, to, field, w) { - if (v.kind === "cell") { - w.append(`${to}.writeCell(${field});`); - } else if (v.kind === "slice") { - w.append(`${to}.writeSlice(${field});`); - } else { - w.append(`${to}.writeBuilder(${field});`); - } + w.append( + `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + ); }, abiMatcher(src) { if (src.kind === "simple") { diff --git a/src/test/e2e-emulated/contracts/serialization-3.tact b/src/test/e2e-emulated/contracts/serialization-3.tact index dceca5cf1..c9214073d 100644 --- a/src/test/e2e-emulated/contracts/serialization-3.tact +++ b/src/test/e2e-emulated/contracts/serialization-3.tact @@ -7,7 +7,7 @@ message Update { f: String; } -contract SerializationTester { +contract SerializationTester3 { a: Int; b: Bool; diff --git a/src/test/e2e-emulated/dns.spec.ts b/src/test/e2e-emulated/dns.spec.ts index 3f75a26d1..4083ccf01 100644 --- a/src/test/e2e-emulated/dns.spec.ts +++ b/src/test/e2e-emulated/dns.spec.ts @@ -87,7 +87,10 @@ describe("dns", () => { const internalAddress = convertToInternal(invalidName); expect( await contract.getDnsInternalVerify( - beginCell().storeBuffer(internalAddress).endCell(), + beginCell() + .storeBuffer(internalAddress) + .endCell() + .asSlice(), ), ).toBe(false); }); @@ -97,8 +100,7 @@ describe("dns", () => { it(`should convert valid name: ${validName}`, async () => { const data = (await contract.getStringToInternal(validName))!; const received = data - .beginParse() - .loadBuffer(data.bits.length / 8) + .loadBuffer(data.remainingBits / 8) .toString("hex"); expect(received).toBe( convertToInternal( @@ -126,16 +128,14 @@ describe("dns", () => { ))!; data1 = await contract.getInternalNormalize(data1); const received1 = data1 - .beginParse() - .loadBuffer(data1.bits.length / 8) + .loadBuffer(data1.remainingBits / 8) .toString("hex"); let data2 = (await contract.getStringToInternal( equalNormalizedElem[1]!, ))!; data2 = await contract.getInternalNormalize(data2); const received2 = data2 - .beginParse() - .loadBuffer(data2.bits.length / 8) + .loadBuffer(data2.remainingBits / 8) .toString("hex"); expect(received1).toBe(received2); expect(received1.length).toBe(received2.length); @@ -148,16 +148,14 @@ describe("dns", () => { ))!; data1 = await contract.getInternalNormalize(data1); const received1 = data1 - .beginParse() - .loadBuffer(data1.bits.length / 8) + .loadBuffer(data1.remainingBits / 8) .toString("hex"); let data2 = (await contract.getStringToInternal( notEqualNormalizedElem[1]!, ))!; data2 = await contract.getInternalNormalize(data2); const received2 = data2 - .beginParse() - .loadBuffer(data2.bits.length / 8) + .loadBuffer(data2.remainingBits / 8) .toString("hex"); expect(received1).not.toBe(received2); expect(received1.length).toBe(received2.length); @@ -168,7 +166,7 @@ describe("dns", () => { it("should resolve name " + validName, async () => { const internalAddress = convertToInternal(validName); const resolved = (await contract.getDnsresolve( - beginCell().storeBuffer(internalAddress).endCell(), + beginCell().storeBuffer(internalAddress).endCell().asSlice(), 1n, ))!; expect(resolved.prefix).toBe(BigInt(internalAddress.length * 8)); @@ -198,7 +196,10 @@ describe("dns", () => { const internalAddress = convertToInternal(invalidName); await expect( contract.getDnsresolve( - beginCell().storeBuffer(internalAddress).endCell(), + beginCell() + .storeBuffer(internalAddress) + .endCell() + .asSlice(), 1n, ), ).rejects.toThrowError(); @@ -216,7 +217,8 @@ describe("dns", () => { .storeBuffer( Buffer.concat([Buffer.alloc(1, 0), internalAddress]), ) - .endCell(), + .endCell() + .asSlice(), 1n, ))!; expect(resolved.prefix).toBe( diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index 1e2080091..11e05e94e 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -75,7 +75,7 @@ describe("intrinsics", () => { expect(await contract.getGetHash2()).toBe(sha256("hello world")); expect( await contract.getGetHash3( - beginCell().storeStringTail("sometest").endCell(), + beginCell().storeStringTail("sometest").endCell().asSlice(), ), ).toBe(sha256("sometest")); expect(await contract.getGetHash4("wallet")).toBe(sha256("wallet")); diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts index b6a986d07..246f9224c 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -35,7 +35,7 @@ describe("local-type-inference", () => { expect((await contract.getTest7()).toString()).toStrictEqual( beginCell().storeUint(123, 64).endCell().toString(), ); - expect((await contract.getTest8()).toString()).toStrictEqual( + expect((await contract.getTest8()).asCell().toString()).toStrictEqual( beginCell().storeUint(123, 64).endCell().toString(), ); expect(await contract.getTest9()).toStrictEqual("hello"); diff --git a/src/test/e2e-emulated/masterchain.spec.ts b/src/test/e2e-emulated/masterchain.spec.ts index f584c285a..893a583cc 100644 --- a/src/test/e2e-emulated/masterchain.spec.ts +++ b/src/test/e2e-emulated/masterchain.spec.ts @@ -108,7 +108,7 @@ describe("masterchain", () => { expect( ( await contract.getParseAddress( - beginCell().storeAddress(addr).endCell(), + beginCell().storeAddress(addr).endCell().asSlice(), ) ).equals(addr), ).toBe(true); @@ -122,7 +122,9 @@ describe("masterchain", () => { await system.run(); const addr = new Address(-1, Buffer.alloc(32, 0)); void expect( - contract.getParseAddress(beginCell().storeAddress(addr).endCell()), + contract.getParseAddress( + beginCell().storeAddress(addr).endCell().asSlice(), + ), ).rejects.toThrowError( "Masterchain support is not enabled for this contract", ); @@ -138,7 +140,7 @@ describe("masterchain", () => { expect( ( await contract.getParseAddress( - beginCell().storeAddress(addr).endCell(), + beginCell().storeAddress(addr).endCell().asSlice(), ) ).equals(addr), ).toBe(true); @@ -154,7 +156,7 @@ describe("masterchain", () => { expect( ( await contract.getParseAddress( - beginCell().storeAddress(addr).endCell(), + beginCell().storeAddress(addr).endCell().asSlice(), ) ).equals(addr), ).toBe(true); diff --git a/src/test/e2e-emulated/math.spec.ts b/src/test/e2e-emulated/math.spec.ts index 68a09686c..0ba7ce52e 100644 --- a/src/test/e2e-emulated/math.spec.ts +++ b/src/test/e2e-emulated/math.spec.ts @@ -19,11 +19,13 @@ describe("math", () => { const sliceA = beginCell() .storeBit(0) .storeRef(beginCell().storeBit(1).endCell()) - .endCell(); + .endCell() + .asSlice(); const sliceB = beginCell() .storeBit(1) .storeRef(beginCell().storeBit(1).endCell()) - .endCell(); + .endCell() + .asSlice(); const stringA = "foo"; const stringB = "bar"; const dictA = Dictionary.empty().set(0n, 0n); diff --git a/src/test/e2e-emulated/serialization.spec.ts b/src/test/e2e-emulated/serialization.spec.ts index 26749ef70..2dd81002a 100644 --- a/src/test/e2e-emulated/serialization.spec.ts +++ b/src/test/e2e-emulated/serialization.spec.ts @@ -1,6 +1,7 @@ -import { toNano } from "@ton/core"; +import { beginCell, Builder, Cell, Slice, toNano } from "@ton/core"; import { ContractSystem } from "@tact-lang/emulator"; import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { SerializationTester3 } from "./contracts/output/serialization-3_SerializationTester3"; import { SerializationTester2 } from "./contracts/output/serialization-2_SerializationTester2"; import { SerializationTester } from "./contracts/output/serialization_SerializationTester"; @@ -158,4 +159,28 @@ describe("serialization", () => { }); } } + it("serialization-3", async () => { + // Init contract + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open( + await SerializationTester3.fromInit( + 1n, + true, + beginCell().endCell(), + beginCell().endCell().asSlice(), + beginCell().endCell().asBuilder(), + "test", + ), + ); + await contract.send(treasure, { value: toNano("10") }, null); + await system.run(); + + expect(await contract.getGetA()).toBe(1n); + expect(await contract.getGetB()).toBe(true); + expect(await contract.getGetC()).toBeInstanceOf(Cell); + expect(await contract.getGetD()).toBeInstanceOf(Slice); + expect(await contract.getGetE()).toBeInstanceOf(Builder); + expect(await contract.getGetF()).toBe("test"); + }); }); diff --git a/src/test/e2e-emulated/stdlib.spec.ts b/src/test/e2e-emulated/stdlib.spec.ts index c6f48cbac..24de72c38 100644 --- a/src/test/e2e-emulated/stdlib.spec.ts +++ b/src/test/e2e-emulated/stdlib.spec.ts @@ -16,7 +16,8 @@ describe("stdlib", () => { .storeBit(1) .storeBit(1) .storeRef(beginCell().storeBit(1).endCell()) - .endCell(); + .endCell() + .asSlice(); expect(await contract.getSliceBits(slice)).toBe(2n); expect(await contract.getSliceRefs(slice)).toBe(1n); expect(await contract.getSliceEmpty(slice)).toBe(false); diff --git a/src/test/e2e-emulated/strings.spec.ts b/src/test/e2e-emulated/strings.spec.ts index 016efd0b4..5b458fdb9 100644 --- a/src/test/e2e-emulated/strings.spec.ts +++ b/src/test/e2e-emulated/strings.spec.ts @@ -63,12 +63,9 @@ describe("strings", () => { expect(await contract.getStringWithFloat()).toEqual("9.5"); const base = await contract.getBase64(); - expect( - base - .beginParse() - .loadBuffer(base.bits.length / 8) - .toString(), - ).toEqual("Many hands make light work."); + expect(base.loadBuffer(base.remainingBits / 8).toString()).toEqual( + "Many hands make light work.", + ); const b64cases = [ "SGVsbG8gV29ybGQ=", @@ -79,7 +76,7 @@ describe("strings", () => { for (const b of b64cases) { const s = Buffer.from(b, "base64"); const r = await contract.getProcessBase64(b); - const d = r.beginParse().loadBuffer(r.bits.length / 8); + const d = r.loadBuffer(r.remainingBits / 8); expect(d.toString("hex")).toEqual(s.toString("hex")); } diff --git a/src/test/e2e-emulated/structs.spec.ts b/src/test/e2e-emulated/structs.spec.ts index e5ea02ebb..6f0a73020 100644 --- a/src/test/e2e-emulated/structs.spec.ts +++ b/src/test/e2e-emulated/structs.spec.ts @@ -131,12 +131,20 @@ describe("structs", () => { expect(await contract.getFromCellMessage1(c5)).toMatchSnapshot(); expect(await contract.getFromCellMessage1(c6)).toMatchSnapshot(); - expect(await contract.getFromSlice1(c1)).toMatchObject(s1); - expect(await contract.getFromSlice1(c2)).toMatchObject(s2); - expect(await contract.getFromSlice2(c3)).toMatchSnapshot(); - expect(await contract.getFromSlice2(c4)).toMatchSnapshot(); - expect(await contract.getFromSliceMessage1(c5)).toMatchSnapshot(); - expect(await contract.getFromSliceMessage1(c6)).toMatchSnapshot(); + expect( + await contract.getFromSlice1(c1.asSlice()), + ).toMatchObject(s1); + expect( + await contract.getFromSlice1(c2.asSlice()), + ).toMatchObject(s2); + expect(await contract.getFromSlice2(c3.asSlice())).toMatchSnapshot(); + expect(await contract.getFromSlice2(c4.asSlice())).toMatchSnapshot(); + expect( + await contract.getFromSliceMessage1(c5.asSlice()), + ).toMatchSnapshot(); + expect( + await contract.getFromSliceMessage1(c6.asSlice()), + ).toMatchSnapshot(); expect((await contract.getTest1(s1, s3)).toString()).toEqual( beginCell().storeRef(c1).storeRef(c3).endCell().toString(),