From 4ef078c5469737b29f21ba20ddf2e19dbfe8ec8a Mon Sep 17 00:00:00 2001 From: Petr Makhnev <51853996+i582@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:30:05 +0400 Subject: [PATCH 1/4] fix: runtime `sha256` works for arbitrary strings with length >= 128 (#1626) --- dev-docs/CHANGELOG.md | 1 + src/abi/global.ts | 10 +- src/generator/Writer.ts | 5 +- src/optimizer/interpreter.ts | 4 +- src/stdlib/stdlib.ts | 6 +- src/stdlib/stdlib/std/stdlib_ex.fc | 15 ++- src/test/benchmarks/benchmarks.spec.ts | 99 ++++++++++++++++++- .../contracts/benchmark_sha256_as_slice.tact | 17 ++++ .../contracts/benchmark_sha256_big.tact | 17 ++++ .../contracts/benchmark_sha256_small.tact | 21 ++++ src/test/e2e-emulated/intrinsics.spec.ts | 83 +++++++++++----- src/types/resolveErrors.ts | 4 +- src/types/resolveSignatures.ts | 12 +-- src/utils/sha256.spec.ts | 32 ++++++ src/utils/sha256.ts | 36 +++++++ 15 files changed, 314 insertions(+), 48 deletions(-) create mode 100644 src/test/benchmarks/contracts/benchmark_sha256_as_slice.tact create mode 100644 src/test/benchmarks/contracts/benchmark_sha256_big.tact create mode 100644 src/test/benchmarks/contracts/benchmark_sha256_small.tact create mode 100644 src/utils/sha256.spec.ts create mode 100644 src/utils/sha256.ts diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index 40410b17a..b254589db 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -76,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Incorrect call generation to a mutation function: PR [#1608](https://github.com/tact-lang/tact/pull/1608) - Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622) - Combine all generated FunC code into a single file: PR [#1698](https://github.com/tact-lang/tact/pull/1698) +- Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626) ### Docs diff --git a/src/abi/global.ts b/src/abi/global.ts index 6aad466a1..ae014f7f5 100644 --- a/src/abi/global.ts +++ b/src/abi/global.ts @@ -10,7 +10,6 @@ import { writeExpression } from "../generator/writers/writeExpression"; import { throwCompilationError } from "../error/errors"; import { getErrorId } from "../types/resolveErrors"; import { AbiFunction } from "./AbiFunction"; -import { sha256_sync } from "@ton/crypto"; import path from "path"; import { cwd } from "process"; import { posixNormalize } from "../utils/filePath"; @@ -20,6 +19,7 @@ import { interpretEscapeSequences, } from "../optimizer/interpreter"; import { isLiteral } from "../ast/ast-helpers"; +import { sha256 } from "../utils/sha256"; export const GlobalFunctions: Map = new Map([ [ @@ -383,14 +383,12 @@ export const GlobalFunctions: Map = new Map([ // FIXME: This one does not need fixing, because it is carried out inside a "isLiteral" check. // Remove this comment once the optimization step is added const str = ensureSimplifiedString(resolved0).value; - return BigInt( - "0x" + sha256_sync(str).toString("hex"), - ).toString(10); + return sha256(str).value.toString(10); } - // Otherwise, revert back to runtime hash through SHA256U + // Otherwise, revert back to runtime hash through HASHEXT_SHA256 const exp = writeExpression(resolved[0]!, ctx); - return `string_hash(${exp})`; + return `__tact_sha256(${exp})`; } // Slice case diff --git a/src/generator/Writer.ts b/src/generator/Writer.ts index 701d6ee62..7f6934d12 100644 --- a/src/generator/Writer.ts +++ b/src/generator/Writer.ts @@ -2,6 +2,7 @@ import { CompilerContext } from "../context/context"; import { escapeUnicodeControlCodes, trimIndent } from "../utils/text"; import { topologicalSort } from "../utils/utils"; import { Writer } from "../utils/Writer"; +import { TactInternalCompilerError } from "../error/errors"; type Flag = "inline" | "impure" | "inline_ref"; @@ -83,10 +84,10 @@ export class WriterContext { } } if (missing.size > 0) { - throw new Error( + throw new TactInternalCompilerError( `Functions ${Array.from(missing.keys()) .map((v) => `"${v}"`) - .join(", ")} wasn't rendered`, + .join(", ")} wasn't processed and generated`, ); } diff --git a/src/optimizer/interpreter.ts b/src/optimizer/interpreter.ts index deccca88d..f1ace6e07 100644 --- a/src/optimizer/interpreter.ts +++ b/src/optimizer/interpreter.ts @@ -21,7 +21,6 @@ import { } from "../types/resolveDescriptors"; import { getExpType } from "../types/resolveExpression"; import { showValue, TypeRef } from "../types/types"; -import { sha256_sync } from "@ton/crypto"; import { defaultParser, getParser, Parser } from "../grammar/grammar"; import { dummySrcInfo, SrcInfo } from "../grammar"; import { @@ -33,6 +32,7 @@ import { isSelfId, } from "../ast/ast-helpers"; import { divFloor, modFloor } from "./util"; +import { sha256 } from "../utils/sha256"; // TVM integers are signed 257-bit integers const minTvmInt: bigint = -(2n ** 256n); @@ -1310,7 +1310,7 @@ export class Interpreter { } const str = ensureSimplifiedString(expr); return this.util.makeNumberLiteral( - BigInt("0x" + sha256_sync(str.value).toString("hex")), + sha256(str.value).value, ast.loc, ); } diff --git a/src/stdlib/stdlib.ts b/src/stdlib/stdlib.ts index aa58896a3..eaca13936 100644 --- a/src/stdlib/stdlib.ts +++ b/src/stdlib/stdlib.ts @@ -563,7 +563,11 @@ files["std/internal/text.tact"] = files["std/stdlib_ex.fc"] = "Zm9yYWxsIFggLT4gdHVwbGUgX190YWN0X3NldCh0dXBsZSB4LCBYIHYsIGludCBpKSBhc20gIlNFVElOREVYVkFSUSI7CigpIF9fdGFjdF9ub3AoKSBhc20gIk5PUCI7" + "CnNsaWNlIF9fdGFjdF9zdHJfdG9fc2xpY2Uoc2xpY2UgcykgYXNtICJOT1AiOwpzbGljZSBfX3RhY3Rfc2xpY2VfdG9fc3RyKHNsaWNlIHMpIGFzbSAiTk9QIjsKc2xp" + - "Y2UgX190YWN0X2FkZHJlc3NfdG9fc2xpY2Uoc2xpY2UgcykgYXNtICJOT1AiOw=="; + "Y2UgX190YWN0X2FkZHJlc3NfdG9fc2xpY2Uoc2xpY2UgcykgYXNtICJOT1AiOwoKKCkgX190YWN0X3NoYV9wdXNoKHNsaWNlIGRhdGEpIGltcHVyZSBhc20gIk9ORSI7" + + "CmludCBfX3RhY3Rfc2hhX3Nob3VsZF9wcm9jZWVkKCkgYXNtICJPVkVSIFNSRUZTIDAgTkVRSU5UIjsKKCkgX190YWN0X3NoYV9vcGVyYXRlKCkgaW1wdXJlIGFzbSAi" + + "T1ZFUiBMRFJFRiBzMCBQT1AgQ1RPUyBzMCBzMSBYQ0hHIElOQyI7CmludCBfX3RhY3Rfc2hhX2hhc2hfZXh0KCkgYXNtICJIQVNIRVhUX1NIQTI1NiI7CgppbnQgX190" + + "YWN0X3NoYTI1NihzbGljZSBkYXRhKSB7CiAgICBfX3RhY3Rfc2hhX3B1c2goZGF0YSk7CiAgICB3aGlsZSAoX190YWN0X3NoYV9zaG91bGRfcHJvY2VlZCgpKSB7CiAg" + + "ICAgICAgX190YWN0X3NoYV9vcGVyYXRlKCk7CiAgICB9CiAgICByZXR1cm4gX190YWN0X3NoYV9oYXNoX2V4dCgpOwp9Cg=="; files["std/stdlib.fc"] = "OzsgU3RhbmRhcmQgbGlicmFyeSBmb3IgZnVuQwo7OwoKey0KICAgIFRoaXMgZmlsZSBpcyBwYXJ0IG9mIFRPTiBGdW5DIFN0YW5kYXJkIExpYnJhcnkuCgogICAgRnVu" + "QyBTdGFuZGFyZCBMaWJyYXJ5IGlzIGZyZWUgc29mdHdhcmU6IHlvdSBjYW4gcmVkaXN0cmlidXRlIGl0IGFuZC9vciBtb2RpZnkKICAgIGl0IHVuZGVyIHRoZSB0ZXJt" + diff --git a/src/stdlib/stdlib/std/stdlib_ex.fc b/src/stdlib/stdlib/std/stdlib_ex.fc index eff126cda..cb69e5740 100644 --- a/src/stdlib/stdlib/std/stdlib_ex.fc +++ b/src/stdlib/stdlib/std/stdlib_ex.fc @@ -2,4 +2,17 @@ forall X -> tuple __tact_set(tuple x, X v, int i) asm "SETINDEXVARQ"; () __tact_nop() asm "NOP"; slice __tact_str_to_slice(slice s) asm "NOP"; slice __tact_slice_to_str(slice s) asm "NOP"; -slice __tact_address_to_slice(slice s) asm "NOP"; \ No newline at end of file +slice __tact_address_to_slice(slice s) asm "NOP"; + +() __tact_sha_push(slice data) impure asm "ONE"; +int __tact_sha_should_proceed() asm "OVER SREFS 0 NEQINT"; +() __tact_sha_operate() impure asm "OVER LDREF s0 POP CTOS s0 s1 XCHG INC"; +int __tact_sha_hash_ext() asm "HASHEXT_SHA256"; + +int __tact_sha256(slice data) { + __tact_sha_push(data); + while (__tact_sha_should_proceed()) { + __tact_sha_operate(); + } + return __tact_sha_hash_ext(); +} diff --git a/src/test/benchmarks/benchmarks.spec.ts b/src/test/benchmarks/benchmarks.spec.ts index 59b549025..7e96881be 100644 --- a/src/test/benchmarks/benchmarks.spec.ts +++ b/src/test/benchmarks/benchmarks.spec.ts @@ -10,9 +10,13 @@ import { SandboxContract, TreasuryContract, } from "@ton/sandbox"; -import { Forward } from "./contracts/output/forward_Forward"; import { Functions } from "./contracts/output/functions_Functions"; +import { Sha256Small } from "./contracts/output/benchmark_sha256_small_Sha256Small"; +import { Sha256Big } from "./contracts/output/benchmark_sha256_big_Sha256Big"; +import { Sha256AsSlice } from "./contracts/output/benchmark_sha256_as_slice_Sha256AsSlice"; +import { Forward } from "./contracts/output/forward_Forward"; import "@ton/test-utils"; +import { getUsedGas } from "./util"; function measureGas(txs: BlockchainTransaction[]) { return ( @@ -67,4 +71,97 @@ describe("benchmarks", () => { const codeSize = testContract.init!.code.toBoc().length; expect(codeSize).toMatchSnapshot("code size"); }); + + async function hashStringSmall( + sha256: SandboxContract, + s: string, + ): Promise { + const result = await sha256.send( + treasure.getSender(), + { value: toNano(1) }, + { $$type: "HashData", value: s }, + ); + + return getUsedGas(result); + } + + async function hashStringBig( + sha256: SandboxContract, + s: string, + ): Promise { + const result = await sha256.send( + treasure.getSender(), + { value: toNano(1) }, + { $$type: "HashData", value: s }, + ); + + return getUsedGas(result); + } + + async function hashStringAsSLice( + sha256: SandboxContract, + s: string, + ): Promise { + const result = await sha256.send( + treasure.getSender(), + { value: toNano(1) }, + { $$type: "HashData", value: s }, + ); + + return getUsedGas(result); + } + + it("benchmark sha256", async () => { + const sha256Small = blockchain.openContract( + await Sha256Small.fromInit(), + ); + const sha256Big = blockchain.openContract(await Sha256Big.fromInit()); + const sha256AsSlice = blockchain.openContract( + await Sha256AsSlice.fromInit(), + ); + + await sha256Small.send( + treasure.getSender(), + { value: toNano(1) }, + null, + ); + await sha256Big.send(treasure.getSender(), { value: toNano(1) }, null); + await sha256AsSlice.send( + treasure.getSender(), + { value: toNano(1) }, + null, + ); + + await hashStringBig(sha256Big, "hello world"); + await hashStringSmall(sha256Small, "hello world"); + await hashStringAsSLice(sha256AsSlice, "hello world"); + + expect(await hashStringBig(sha256Big, "hello world")).toEqual(3039n); + expect(await hashStringSmall(sha256Small, "hello world")).toEqual( + 2516n, + ); + expect(await hashStringAsSLice(sha256AsSlice, "hello world")).toEqual( + 2516n, + ); + + expect(await hashStringBig(sha256Big, "hello world".repeat(5))).toEqual( + 3040n, + ); + expect( + await hashStringSmall(sha256Small, "hello world".repeat(5)), + ).toEqual(2516n); + expect( + await hashStringAsSLice(sha256AsSlice, "hello world".repeat(5)), + ).toEqual(2516n); + + expect( + await hashStringBig(sha256Big, "hello world".repeat(10)), + ).toEqual(3042n); + expect( + await hashStringSmall(sha256Small, "hello world".repeat(10)), + ).toEqual(2516n); + expect( + await hashStringAsSLice(sha256AsSlice, "hello world".repeat(10)), + ).toEqual(2516n); + }); }); diff --git a/src/test/benchmarks/contracts/benchmark_sha256_as_slice.tact b/src/test/benchmarks/contracts/benchmark_sha256_as_slice.tact new file mode 100644 index 000000000..1b2ff0121 --- /dev/null +++ b/src/test/benchmarks/contracts/benchmark_sha256_as_slice.tact @@ -0,0 +1,17 @@ +message HashData { + value: String; +} + +contract Sha256AsSlice { + result: Int = 0; + + receive() {} + + receive(h: HashData) { + self.result += sha256(h.value.asSlice()); + } + + get fun res(): Int { + return self.result; + } +} diff --git a/src/test/benchmarks/contracts/benchmark_sha256_big.tact b/src/test/benchmarks/contracts/benchmark_sha256_big.tact new file mode 100644 index 000000000..9ffaff373 --- /dev/null +++ b/src/test/benchmarks/contracts/benchmark_sha256_big.tact @@ -0,0 +1,17 @@ +message HashData { + value: String; +} + +contract Sha256Big { + result: Int = 0; + + receive() {} + + receive(h: HashData) { + self.result += sha256(h.value); + } + + get fun res(): Int { + return self.result; + } +} diff --git a/src/test/benchmarks/contracts/benchmark_sha256_small.tact b/src/test/benchmarks/contracts/benchmark_sha256_small.tact new file mode 100644 index 000000000..00cb9dc30 --- /dev/null +++ b/src/test/benchmarks/contracts/benchmark_sha256_small.tact @@ -0,0 +1,21 @@ +asm fun sha256_native(s: String): Int { + SHA256U +} + +message HashData { + value: String; +} + +contract Sha256Small { + result: Int = 0; + + receive() {} + + receive(h: HashData) { + self.result += sha256_native(h.value); + } + + get fun res(): Int { + return self.result; + } +} diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index 95a77a0be..ed180ea81 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -1,9 +1,9 @@ import { Address, beginCell, Cell, toNano } from "@ton/core"; import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { IntrinsicsTester } from "./contracts/output/intrinsics_IntrinsicsTester"; -import { sha256_sync } from "@ton/crypto"; import "@ton/test-utils"; import { paddedBufferToBits } from "@ton/core/dist/boc/utils/paddedBits"; +import { sha256 } from "../../utils/sha256"; describe("intrinsics", () => { let blockchain: Blockchain; @@ -94,29 +94,6 @@ describe("intrinsics", () => { expect(outMessage.remainingBits).toEqual(0); expect(outMessage.remainingRefs).toEqual(0); - // Check sha256 - function sha256(src: string | Buffer) { - return BigInt("0x" + sha256_sync(src).toString("hex")); - } - expect(await contract.getGetHash()).toBe(sha256("hello world")); - expect(await contract.getGetHash2()).toBe(sha256("hello world")); - expect( - await contract.getGetHash3( - beginCell().storeStringTail("sometest").endCell().asSlice(), - ), - ).toBe(sha256("sometest")); - expect(await contract.getGetHash4("wallet")).toBe(sha256("wallet")); - const longString = - "------------------------------------------------------------------------------------------------------------------------------129"; - expect(await contract.getGetHashLongComptime()).toBe( - sha256(longString), - ); - // NOTE: The discrepancy here is expected, since SHA256U operates only on the first 127 bytes - expect( - (await contract.getGetHashLongRuntime(longString)) !== - sha256(longString), - ).toBe(true); - // Check `slice` expect( (await contract.getGetSlice()) @@ -285,4 +262,62 @@ describe("intrinsics", () => { expect(await contract.getGetCrc32_3()).toBe(0n); expect(await contract.getGetCrc32_4()).toBe(0n); }); + + const checkSha256 = async (input: string) => { + const expected = sha256(input).value; + const actual = await contract.getGetHashLongRuntime(input); + expect(actual.toString(16)).toEqual(expected.toString(16)); + }; + + const generateString = (length: number): string => { + const chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + let result = ""; + for (let i = 0; i < length; i++) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; + }; + + it("should calculate sha256 correctly", async () => { + function sha256Hex(src: string | Buffer) { + return sha256(src).value; + } + expect(await contract.getGetHash()).toBe(sha256Hex("hello world")); + expect(await contract.getGetHash2()).toBe(sha256Hex("hello world")); + expect( + await contract.getGetHash3( + beginCell().storeStringTail("sometest").endCell().asSlice(), + ), + ).toBe(sha256Hex("sometest")); + expect(await contract.getGetHash4("wallet")).toBe(sha256Hex("wallet")); + const longString = + "------------------------------------------------------------------------------------------------------------------------------129"; + expect(await contract.getGetHashLongComptime()).toBe( + sha256Hex(longString), + ); + + await checkSha256("hello world"); + + const input256bytes = generateString(256); + + // check various length input + await checkSha256(generateString(15)); + await checkSha256(generateString(127)); + await checkSha256(generateString(128)); + await checkSha256(input256bytes); + await checkSha256(generateString(1024)); + await checkSha256(generateString(16999)); + + // check that we hash all string, not just first 127 bytes + const first128bytesOf256bytesString = input256bytes.slice(0, 128); + const first128bytesOf256bytesStringHash = + await contract.getGetHashLongRuntime(first128bytesOf256bytesString); + const input256bytesStringHash = + await contract.getGetHashLongRuntime(input256bytes); + + expect(first128bytesOf256bytesStringHash).not.toEqual( + input256bytesStringHash, + ); + }); }); diff --git a/src/types/resolveErrors.ts b/src/types/resolveErrors.ts index 06e2a4cb1..2c88539a1 100644 --- a/src/types/resolveErrors.ts +++ b/src/types/resolveErrors.ts @@ -1,4 +1,3 @@ -import { sha256_sync } from "@ton/crypto"; import { CompilerContext, createContextStore } from "../context/context"; import { AstNode } from "../ast/ast"; import { FactoryAst, isRequire } from "../ast/ast-helpers"; @@ -12,13 +11,14 @@ import { } from "./resolveDescriptors"; import { ensureSimplifiedString } from "../optimizer/interpreter"; import { AstUtil, getAstUtil } from "../ast/util"; +import { sha256, highest32ofSha256 } from "../utils/sha256"; type Exception = { value: string; id: number }; const exceptions = createContextStore(); function stringId(src: string): number { - return sha256_sync(src).readUInt32BE(0); + return Number(highest32ofSha256(sha256(src))); } function exceptionId(src: string): number { diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index 5d579e3c6..bc47f4684 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -1,5 +1,5 @@ import * as changeCase from "change-case"; -import { ABIField, beginCell } from "@ton/core"; +import { ABIField } from "@ton/core"; import { CompilerContext } from "../context/context"; import { idToHex } from "../utils/idToHex"; import { @@ -18,11 +18,11 @@ import { throwCompilationError } from "../error/errors"; import { AstNumber, AstReceiver } from "../ast/ast"; import { FactoryAst } from "../ast/ast-helpers"; import { commentPseudoOpcode } from "../generator/writers/writeRouter"; -import { sha256_sync } from "@ton/crypto"; import { dummySrcInfo } from "../grammar"; import { ensureInt } from "../optimizer/interpreter"; import { evalConstantExpression } from "../optimizer/constEval"; import { getAstUtil } from "../ast/util"; +import { sha256, highest32ofSha256 } from "../utils/sha256"; export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) { const util = getAstUtil(Ast); @@ -284,13 +284,7 @@ function newMessageOpcode(signature: string): AstNumber { return { kind: "number", base: 10, - value: BigInt( - beginCell() - .storeBuffer(sha256_sync(signature)) - .endCell() - .beginParse() - .loadUint(32), - ), + value: highest32ofSha256(sha256(signature)), id: 0, loc: dummySrcInfo, }; diff --git a/src/utils/sha256.spec.ts b/src/utils/sha256.spec.ts new file mode 100644 index 000000000..34f9b26c7 --- /dev/null +++ b/src/utils/sha256.spec.ts @@ -0,0 +1,32 @@ +import { sha256, highest32ofSha256 } from "./sha256"; +import { beginCell } from "@ton/core"; + +describe("sha256", () => { + const bigintToBuffer = (value: bigint): Buffer => { + const hex = value.toString(16); + const paddedHex = hex.length % 2 === 0 ? hex : "0" + hex; + return Buffer.from(paddedHex, "hex"); + }; + + it("should be equal to Buffer.readUInt32BE", () => { + const res = sha256("hello world"); + const buffer = bigintToBuffer(res.value); + + expect(highest32ofSha256(res)).toBe(BigInt(buffer.readUInt32BE())); + }); + + it("should be equal to storeBuffer().loadUint", () => { + const res = sha256("hello world"); + const buffer = bigintToBuffer(res.value); + + expect(highest32ofSha256(res)).toBe( + BigInt( + beginCell() + .storeBuffer(buffer) + .endCell() + .beginParse() + .loadUint(32), + ), + ); + }); +}); diff --git a/src/utils/sha256.ts b/src/utils/sha256.ts new file mode 100644 index 000000000..ae2c40ef4 --- /dev/null +++ b/src/utils/sha256.ts @@ -0,0 +1,36 @@ +import { sha256_sync } from "@ton/crypto"; + +// Witness tag. Do not use, do not export! +const Sha256Tag = Symbol("sha256"); + +type Sha256 = { + readonly kind: typeof Sha256Tag; + readonly value: bigint; +}; + +export const sha256 = (input: Buffer | string): Sha256 => ({ + kind: Sha256Tag, + value: bufferToBigInt(sha256_sync(input)), +}); + +/** + * Extracts the most significant 32 bits from a `BigInt` value representing an SHA-256 hash. + * + * Example 1: + * ```typescript + * sha.value = BigInt(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef) + * ``` + * `sha256LoadUint32BE(sha)` will return `0x12345678`, as the top 32 bits are `0x12345678`. + * + * + * Example 2: + * ```typescript + * sha.value = BigInt(0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba) + * ```` + * `sha256LoadUint32BE(sha)` will return `0x98765432`, as the top 32 bits are `0x98765432`. + */ +export const highest32ofSha256 = (sha: Sha256): bigint => + sha.value >> (256n - 32n); + +const bufferToBigInt = (buffer: Buffer): bigint => + buffer.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n); From 50612f95738b14d0a9b8945eef7d84a6681cc1a8 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:59:43 +0100 Subject: [PATCH 2/4] docs: mark gas-expensive (>= 500 gas units) functions and expressions (#1703) --- dev-docs/CHANGELOG.md | 1 + docs/src/content/docs/book/expressions.mdx | 4 ++++ docs/src/content/docs/book/maps.mdx | 1 + docs/src/content/docs/ref/core-advanced.mdx | 1 + docs/src/content/docs/ref/core-cells.mdx | 10 ++++++++++ docs/src/content/docs/ref/core-common.mdx | 12 ++++++++++++ docs/src/content/docs/ref/core-debug.mdx | 6 ++++++ docs/src/content/docs/ref/core-math.mdx | 6 ++++++ docs/src/content/docs/ref/core-strings.mdx | 18 ++++++++++++++++++ 9 files changed, 59 insertions(+) diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index b254589db..e58285445 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -112,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved Chinese localization of the documentation: PR [#1642](https://github.com/tact-lang/tact/pull/1642) - Removed the notion of the non-standard TL-B syntax `remainder`: PR [#1599](https://github.com/tact-lang/tact/pull/1599) - Added description of `.boc`, `.ts`, `.abi`, `.pkg` files and completed Compilation page: PR [#1676](https://github.com/tact-lang/tact/pull/1676) +- Marked gas-expensive functions and expressions: PR [#1703](https://github.com/tact-lang/tact/pull/1703) ### Release contributors diff --git a/docs/src/content/docs/book/expressions.mdx b/docs/src/content/docs/book/expressions.mdx index 8048c85a5..808b13cc7 100644 --- a/docs/src/content/docs/book/expressions.mdx +++ b/docs/src/content/docs/book/expressions.mdx @@ -3,6 +3,8 @@ title: Expressions description: "This page lists all the expressions in Tact" --- +import { Badge } from '@astrojs/starlight/components'; + Every operator in Tact forms an expression, but there's much more to uncover as Tact offers a wide range of expressive options to choose from. :::note @@ -252,6 +254,8 @@ contract ExampleContract { ## `initOf` +

+ Expression `initOf{:tact}` computes initial state, i.e. `StateInit{:tact}` of a [contract](/book/contracts): ```tact diff --git a/docs/src/content/docs/book/maps.mdx b/docs/src/content/docs/book/maps.mdx index ea9565b71..c47d52471 100644 --- a/docs/src/content/docs/book/maps.mdx +++ b/docs/src/content/docs/book/maps.mdx @@ -359,6 +359,7 @@ if (fizz == null) { ### Compare with `.deepEquals()` {#deepequals} +

```tact diff --git a/docs/src/content/docs/ref/core-advanced.mdx b/docs/src/content/docs/ref/core-advanced.mdx index 412367e38..39df2de14 100644 --- a/docs/src/content/docs/ref/core-advanced.mdx +++ b/docs/src/content/docs/ref/core-advanced.mdx @@ -444,6 +444,7 @@ Attempts to queue more than $255$ messages throw an exception with an [exit code ## nativeSendMessageReturnForwardFee +

```tact diff --git a/docs/src/content/docs/ref/core-cells.mdx b/docs/src/content/docs/ref/core-cells.mdx index b22b50613..a0e9f11c4 100644 --- a/docs/src/content/docs/ref/core-cells.mdx +++ b/docs/src/content/docs/ref/core-cells.mdx @@ -35,6 +35,8 @@ let fizz: Builder = beginCell(); ## emptyCell +

+ ```tact fun emptyCell(): Cell; ``` @@ -52,6 +54,8 @@ fizz == buzz; // true ## emptySlice +

+ ```tact fun emptySlice(): Slice; ``` @@ -128,6 +132,8 @@ A section to group all extension and extension mutation functions for the [`Buil ### Builder.endCell +

+ ```tact extends fun endCell(self: Builder): Cell; ``` @@ -1035,6 +1041,8 @@ try { ### Slice.hash +

+ ```tact extends fun hash(self: Slice): Int; ``` @@ -1052,6 +1060,8 @@ let fizz: Int = s.hash(); ### Slice.asCell +

+ ```tact extends fun asCell(self: Slice): Cell; ``` diff --git a/docs/src/content/docs/ref/core-common.mdx b/docs/src/content/docs/ref/core-common.mdx index e93ee2955..021c68478 100644 --- a/docs/src/content/docs/ref/core-common.mdx +++ b/docs/src/content/docs/ref/core-common.mdx @@ -3,6 +3,8 @@ title: Common description: "Commonly used global static functions from the Core library of Tact" --- +import { Badge } from '@astrojs/starlight/components'; + List of the most commonly used built-in [global static functions](/book/functions#global-static-functions). ## Contextual @@ -117,6 +119,8 @@ require(ctx.value != 68 + 1, "Invalid amount of nanoToncoins, bye!"); ### newAddress +

+ ```tact fun newAddress(chain: Int, hash: Int): Address; ``` @@ -152,6 +156,8 @@ let oldTonFoundationAddr: Address = ### contractAddress +

+ ```tact fun contractAddress(s: StateInit): Address; ``` @@ -166,6 +172,8 @@ let foundMeSome: Address = contractAddress(initOf SomeContract()); ### contractAddressExt +

+ ```tact fun contractAddressExt(chain: Int, code: Cell, data: Cell): Address; ``` @@ -196,6 +204,8 @@ let hereBeDragons: Address = contractAddressExt(0, initPkg.code, initPkg.data); ### send +

+ ```tact fun send(params: SendParameters); ``` @@ -224,6 +234,8 @@ send(SendParameters{ ### emit +

+ ```tact fun emit(body: Cell); ``` diff --git a/docs/src/content/docs/ref/core-debug.mdx b/docs/src/content/docs/ref/core-debug.mdx index 3b05cb81a..5bdb9dfc7 100644 --- a/docs/src/content/docs/ref/core-debug.mdx +++ b/docs/src/content/docs/ref/core-debug.mdx @@ -3,6 +3,8 @@ title: Debug description: "Various debugging functions from the Core library of Tact" --- +import { Badge } from '@astrojs/starlight/components'; + List of functions commonly used for debugging smart contracts in Tact. Read more about debugging on the dedicated page: [Debugging](/book/debug). @@ -40,6 +42,8 @@ try { ## dump +

+ ```tact fun dump(arg); ``` @@ -102,6 +106,8 @@ dump(emit("msg".asComment())); // As emit() function doesn't return a value, dum ## dumpStack +

+ ```tact fun dumpStack(); ``` diff --git a/docs/src/content/docs/ref/core-math.mdx b/docs/src/content/docs/ref/core-math.mdx index e4ccb1f81..9eb3b10a9 100644 --- a/docs/src/content/docs/ref/core-math.mdx +++ b/docs/src/content/docs/ref/core-math.mdx @@ -3,6 +3,8 @@ title: Math description: "Various math helper functions from the Core library of Tact" --- +import { Badge } from '@astrojs/starlight/components'; + Various math helper functions. ## min @@ -191,6 +193,8 @@ contract Example { ## checkSignature +

+ ```tact fun checkSignature(hash: Int, signature: Slice, public_key: Int): Bool; ``` @@ -232,6 +236,8 @@ contract Showcase { ## checkDataSignature +

+ ```tact fun checkDataSignature(data: Slice, signature: Slice, public_key: Int): Bool; ``` diff --git a/docs/src/content/docs/ref/core-strings.mdx b/docs/src/content/docs/ref/core-strings.mdx index 3e836cbee..b256b16ff 100644 --- a/docs/src/content/docs/ref/core-strings.mdx +++ b/docs/src/content/docs/ref/core-strings.mdx @@ -3,6 +3,8 @@ title: Strings and StringBuilders description: "Various String and StringBuilder functions from the Core library of Tact" --- +import { Badge } from '@astrojs/starlight/components'; + Strings are immutable sequences of characters, which means that once a [`String{:tact}`][p] is created, it cannot be changed. Strings are useful to store text, and so they can be converted to [`Cell{:tact}`][cell] type to be used as message bodies. To be able to concatenate strings in a gas-efficient way, use a [`StringBuilder{:tact}`][p]. @@ -105,6 +107,8 @@ let fizz: StringBuilder = beginString() ## StringBuilder.toString +

+ ```tact extends fun toString(self: StringBuilder): String; ``` @@ -122,6 +126,8 @@ let buzz: String = fizz.toString(); ## StringBuilder.toCell +

+ ```tact extends fun toCell(self: StringBuilder): Cell; ``` @@ -139,6 +145,8 @@ let buzz: Cell = fizz.toCell(); ## StringBuilder.toSlice +

+ ```tact extends fun toSlice(self: StringBuilder): Slice; ``` @@ -187,6 +195,8 @@ fizz == buzz; // true, but be careful as it's not always the case ## String.asComment +

+ ```tact extends fun asComment(self: String): Cell; ``` @@ -277,6 +287,8 @@ let fizz: Slice = s.fromBase64(); ## Int.toString +

+ ```tact extends fun toString(self: Int): String; ``` @@ -293,6 +305,8 @@ let fizz: String = (84 - 42).toString(); ## Int.toFloatString +

+ ```tact extends fun toFloatString(self: Int, digits: Int): String; ``` @@ -311,6 +325,8 @@ let fizz: String = (42).toFloatString(9); // "0.000000042" ## Int.toCoinsString +

+ ```tact extends fun toCoinsString(self: Int): String; ``` @@ -333,6 +349,8 @@ fizz == buzz; // true, both store "0.000000042" ## Address.toString +

+ ```tact extends fun toString(self: Address): String; ``` From 7f3bb273907002a2d6eb65f53c80e9cab1a9aef1 Mon Sep 17 00:00:00 2001 From: Petr Makhnev <51853996+i582@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:00:43 +0400 Subject: [PATCH 3/4] fix: error message for comment (text) receivers with 124 bytes or more (#1711) --- dev-docs/CHANGELOG.md | 1 + docs/src/content/docs/book/receive.mdx | 2 +- src/generator/writers/writeRouter.ts | 22 ++- .../resolveDescriptors.spec.ts.snap | 185 ++++++++++++++++++ src/types/resolveSignatures.ts | 2 +- .../receive-message-too-big-2.tact | 5 + .../test-failed/receive-message-too-big.tact | 5 + ...ive-message-with-biggest-allowed-size.tact | 5 + 8 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 src/types/test-failed/receive-message-too-big-2.tact create mode 100644 src/types/test-failed/receive-message-too-big.tact create mode 100644 src/types/test/receive-message-with-biggest-allowed-size.tact diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index e58285445..6eb7eb45c 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -77,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622) - Combine all generated FunC code into a single file: PR [#1698](https://github.com/tact-lang/tact/pull/1698) - Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626) +- Error message for comment (text) receivers with 124 bytes or more: PR [#1711](https://github.com/tact-lang/tact/pull/1711) ### Docs diff --git a/docs/src/content/docs/book/receive.mdx b/docs/src/content/docs/book/receive.mdx index d9469f8fd..4fa27a157 100644 --- a/docs/src/content/docs/book/receive.mdx +++ b/docs/src/content/docs/book/receive.mdx @@ -15,7 +15,7 @@ To receive a message of the required type, you need to declare a receiver functi There are several receiver functions. All receiver functions are processed in the order they are listed below: * `receive(){:tact}` - called when an empty message is sent to the contract -* `receive("message"){:tact}` - called when a text message with a specific comment is sent to the contract +* `receive("message"){:tact}` - called when a text message with a specific comment is sent to the contract (maximum `"message"{:tact}` length is 123 bytes) * `receive(str: String){:tact}` - called when an arbitrary text message is sent to the contract * `receive(msg: MyMessage){:tact}` - called when a binary message of type `MyMessage` is sent to the contract * `receive(msg: Slice){:tact}` - called when binary message of unknown type is sent to the contract diff --git a/src/generator/writers/writeRouter.ts b/src/generator/writers/writeRouter.ts index 89aa08d85..023698348 100644 --- a/src/generator/writers/writeRouter.ts +++ b/src/generator/writers/writeRouter.ts @@ -7,12 +7,21 @@ import { ops } from "./ops"; import { resolveFuncType } from "./resolveFuncType"; import { resolveFuncTypeUnpack } from "./resolveFuncTypeUnpack"; import { writeStatement } from "./writeFunction"; -import { AstNumber } from "../../ast/ast"; +import { AstNumber, AstReceiver } from "../../ast/ast"; +import { throwCompilationError } from "../../error/errors"; + +export function commentPseudoOpcode(comment: string, ast: AstReceiver): string { + const buffer = Buffer.from(comment, "utf8"); + if (buffer.length > 123) { + throwCompilationError( + `receiver message is too long, max length is 123 bytes, but given ${buffer.length}`, + ast.loc, + ); + } -export function commentPseudoOpcode(comment: string): string { return beginCell() .storeUint(0, 32) - .storeBuffer(Buffer.from(comment, "utf8")) + .storeBuffer(buffer) .endCell() .hash() .toString("hex", 0, 64); @@ -206,7 +215,10 @@ export function writeRouter( selector.kind === (internal ? "internal-comment" : "external-comment") ) { - const hash = commentPseudoOpcode(selector.comment); + const hash = commentPseudoOpcode( + selector.comment, + r.ast, + ); ctx.append(); ctx.append( `;; Receive "${selector.comment}" message`, @@ -365,7 +377,7 @@ export function writeReceiver( selector.kind === "internal-comment" || selector.kind === "external-comment" ) { - const hash = commentPseudoOpcode(selector.comment); + const hash = commentPseudoOpcode(selector.comment, f.ast); ctx.append( `(${selfType}, ()) ${ops.receiveText(self.name, selector.kind === "internal-comment" ? "internal" : "external", hash)}(${selfType + " " + funcIdOf("self")}) impure inline {`, ); diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index cbc4976f2..047d27008 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -612,6 +612,24 @@ exports[`resolveDescriptors should fail descriptors for read-self-in-init-functi " `; +exports[`resolveDescriptors should fail descriptors for receive-message-too-big 1`] = ` +":4:5: receiver message is too long, max length is 123 bytes, but given 124 + 3 | contract Foo { +> 4 | receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {} + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 5 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for receive-message-too-big-2 1`] = ` +":4:5: receiver message is too long, max length is 123 bytes, but given 156 + 3 | contract Foo { +> 4 | receive("💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀") {} + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 5 | } +" +`; + exports[`resolveDescriptors should fail descriptors for scope-contract-shadows-contract 1`] = ` ":12:1: Type "Main" already exists 11 | @@ -23708,6 +23726,173 @@ exports[`resolveDescriptors should resolve descriptors for override-for-existing exports[`resolveDescriptors should resolve descriptors for override-for-existing-function-in-trait-with-multi-inheritance-from-base-trait 2`] = `[]`; +exports[`resolveDescriptors should resolve descriptors for receive-message-with-biggest-allowed-size 1`] = ` +[ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 2, + "kind": "trait", + "loc": trait BaseTrait {}, + "name": { + "id": 1, + "kind": "id", + "loc": BaseTrait, + "text": "BaseTrait", + }, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + { + "ast": { + "attributes": [], + "declarations": [ + { + "id": 7, + "kind": "receiver", + "loc": receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {}, + "selector": { + "id": 6, + "kind": "internal", + "loc": receive, + "subKind": { + "comment": { + "id": 4, + "kind": "string", + "loc": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + "id": 5, + "kind": "comment", + }, + }, + "statements": [], + }, + ], + "id": 8, + "kind": "contract", + "loc": contract Foo { + receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {} +}, + "name": { + "id": 3, + "kind": "id", + "loc": Foo, + "text": "Foo", + }, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": { + "ast": { + "id": 10, + "kind": "contract_init", + "loc": contract Foo { + receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {} +}, + "params": [], + "statements": [], + }, + "params": [], + }, + "interfaces": [], + "kind": "contract", + "name": "Foo", + "origin": "user", + "partialFieldCount": 0, + "receivers": [ + { + "ast": { + "id": 7, + "kind": "receiver", + "loc": receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {}, + "selector": { + "id": 6, + "kind": "internal", + "loc": receive, + "subKind": { + "comment": { + "id": 4, + "kind": "string", + "loc": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + "id": 5, + "kind": "comment", + }, + }, + "statements": [], + }, + "selector": { + "comment": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "kind": "internal-comment", + }, + }, + ], + "signature": null, + "tlb": null, + "traits": [ + { + "ast": { + "attributes": [], + "declarations": [], + "id": 2, + "kind": "trait", + "loc": trait BaseTrait {}, + "name": { + "id": 1, + "kind": "id", + "loc": BaseTrait, + "text": "BaseTrait", + }, + "traits": [], + }, + "constants": [], + "dependsOn": [], + "fields": [], + "functions": Map {}, + "header": null, + "init": null, + "interfaces": [], + "kind": "trait", + "name": "BaseTrait", + "origin": "user", + "partialFieldCount": 0, + "receivers": [], + "signature": null, + "tlb": null, + "traits": [], + "uid": 1020, + }, + ], + "uid": 10576, + }, +] +`; + +exports[`resolveDescriptors should resolve descriptors for receive-message-with-biggest-allowed-size 2`] = `[]`; + exports[`resolveDescriptors should resolve descriptors for scope-loops 1`] = `[]`; exports[`resolveDescriptors should resolve descriptors for scope-loops 2`] = ` diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index bc47f4684..85e53b214 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -319,7 +319,7 @@ function checkCommentMessageReceiver( rcvAst: AstReceiver, usedOpcodes: Map, ) { - const opcode = commentPseudoOpcode(rcv.comment); + const opcode = commentPseudoOpcode(rcv.comment, rcvAst); if (usedOpcodes.has(opcode)) { throwCompilationError( `Receive functions of a contract or trait cannot process comments with the same hashes: hashes of comment strings "${rcv.comment}" and "${usedOpcodes.get(opcode)}" are equal`, diff --git a/src/types/test-failed/receive-message-too-big-2.tact b/src/types/test-failed/receive-message-too-big-2.tact new file mode 100644 index 000000000..9bffce9dc --- /dev/null +++ b/src/types/test-failed/receive-message-too-big-2.tact @@ -0,0 +1,5 @@ +trait BaseTrait {} + +contract Foo { + receive("💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀") {} +} diff --git a/src/types/test-failed/receive-message-too-big.tact b/src/types/test-failed/receive-message-too-big.tact new file mode 100644 index 000000000..465ae58c9 --- /dev/null +++ b/src/types/test-failed/receive-message-too-big.tact @@ -0,0 +1,5 @@ +trait BaseTrait {} + +contract Foo { + receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {} +} diff --git a/src/types/test/receive-message-with-biggest-allowed-size.tact b/src/types/test/receive-message-with-biggest-allowed-size.tact new file mode 100644 index 000000000..f2eed6541 --- /dev/null +++ b/src/types/test/receive-message-with-biggest-allowed-size.tact @@ -0,0 +1,5 @@ +trait BaseTrait {} + +contract Foo { + receive("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") {} +} From 524b030fb2afd7586ebcf2e140deeff933aaae59 Mon Sep 17 00:00:00 2001 From: Petr Makhnev <51853996+i582@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:18:37 +0400 Subject: [PATCH 4/4] fix(wrappers): generated code for contract with `init(init: Init)` (#1709) --- dev-docs/CHANGELOG.md | 1 + src/bindings/writeTypescript.ts | 6 +-- .../contract-with-init-init-parameter.spec.ts | 40 +++++++++++++++++++ .../contract-with-init-init-parameter.tact | 15 +++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/test/e2e-emulated/contract-with-init-init-parameter.spec.ts create mode 100644 src/test/e2e-emulated/contracts/contract-with-init-init-parameter.tact diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index 6eb7eb45c..7303c8b87 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -77,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow constant/trait constants depend on each other: PR [#1622](https://github.com/tact-lang/tact/pull/1622) - Combine all generated FunC code into a single file: PR [#1698](https://github.com/tact-lang/tact/pull/1698) - Runtime `sha256` now work for arbitrary strings with length >= 128: PR [#1626](https://github.com/tact-lang/tact/pull/1626) +- Generated code in TypeScript wrappers for contract with `init(init: Init)`: PR [#1709](https://github.com/tact-lang/tact/pull/1709) - Error message for comment (text) receivers with 124 bytes or more: PR [#1711](https://github.com/tact-lang/tact/pull/1711) ### Docs diff --git a/src/bindings/writeTypescript.ts b/src/bindings/writeTypescript.ts index 9063c38be..3e985c389 100644 --- a/src/bindings/writeTypescript.ts +++ b/src/bindings/writeTypescript.ts @@ -290,10 +290,10 @@ export function writeTypescript( ); w.inIndent(() => { w.append( - `const init = await ${abi.name}_init(${init!.args.map((v) => v.name).join(", ")});`, + `const __gen_init = await ${abi.name}_init(${init!.args.map((v) => v.name).join(", ")});`, ); - w.append(`const address = contractAddress(0, init);`); - w.append(`return new ${abi.name}(address, init);`); + w.append(`const address = contractAddress(0, __gen_init);`); + w.append(`return new ${abi.name}(address, __gen_init);`); }); w.append(`}`); w.append(); diff --git a/src/test/e2e-emulated/contract-with-init-init-parameter.spec.ts b/src/test/e2e-emulated/contract-with-init-init-parameter.spec.ts new file mode 100644 index 000000000..8b8289a6c --- /dev/null +++ b/src/test/e2e-emulated/contract-with-init-init-parameter.spec.ts @@ -0,0 +1,40 @@ +import { toNano } from "@ton/core"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { Test } from "./contracts/output/contract-with-init-init-parameter_Test"; +import "@ton/test-utils"; + +describe("contract-with-init-init-parameter", () => { + let blockchain: Blockchain; + let treasure: SandboxContract; + let contract: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasure = await blockchain.treasury("treasure"); + + contract = blockchain.openContract( + await Test.fromInit({ + $$type: "Init", + foo: 99n, + }), + ); + + const deployResult = await contract.send( + treasure.getSender(), + { value: toNano("0.5") }, + null, + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: true, + deploy: true, + }); + }); + + it("should return correct result", async () => { + expect(await contract.getData()).toBe(99n); + }); +}); diff --git a/src/test/e2e-emulated/contracts/contract-with-init-init-parameter.tact b/src/test/e2e-emulated/contracts/contract-with-init-init-parameter.tact new file mode 100644 index 000000000..f3554c888 --- /dev/null +++ b/src/test/e2e-emulated/contracts/contract-with-init-init-parameter.tact @@ -0,0 +1,15 @@ +struct Init { foo: Int as uint8 } + +contract Test { + foo: Int; + + init(init: Init) { + self.foo = init.foo; + } + + receive() {} + + get fun data(): Int { + return self.foo; + } +}