From 32a4dceb47413c1ed92685baf04a9d9417dc6e01 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Wed, 30 Apr 2025 23:32:40 +0200 Subject: [PATCH 01/10] feat: allow a special CLI argument `--quick-start` and add a special template with counter and open-ended poll --- src/index.ts | 91 +++++++++++-------- template/empty/tact.config.json | 7 ++ .../quick-start/.github/workflows/pull.yml | 50 ++++++++++ template/quick-start/.prettierrc.json | 5 + template/quick-start/.vscode/extensions.json | 3 + template/quick-start/README.md | 36 ++++++++ template/quick-start/deploy.ts | 21 +++++ template/quick-start/gitignore | 7 ++ template/quick-start/jest.config.ts | 6 ++ template/quick-start/package.json | 31 +++++++ template/quick-start/src/counter.spec.ts | 22 +++++ template/quick-start/src/counter.tact | 38 ++++++++ template/quick-start/src/poll.spec.ts | 22 +++++ template/quick-start/src/poll.tact | 70 ++++++++++++++ template/quick-start/tact.config.json | 41 +++++++++ template/quick-start/tsconfig.json | 65 +++++++++++++ 16 files changed, 476 insertions(+), 39 deletions(-) create mode 100644 template/quick-start/.github/workflows/pull.yml create mode 100644 template/quick-start/.prettierrc.json create mode 100644 template/quick-start/.vscode/extensions.json create mode 100644 template/quick-start/README.md create mode 100644 template/quick-start/deploy.ts create mode 100644 template/quick-start/gitignore create mode 100644 template/quick-start/jest.config.ts create mode 100644 template/quick-start/package.json create mode 100644 template/quick-start/src/counter.spec.ts create mode 100644 template/quick-start/src/counter.tact create mode 100644 template/quick-start/src/poll.spec.ts create mode 100644 template/quick-start/src/poll.tact create mode 100644 template/quick-start/tact.config.json create mode 100644 template/quick-start/tsconfig.json diff --git a/src/index.ts b/src/index.ts index 354f07b..bdcdaec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises"; import { dirname, join, relative } from "path"; import { createInterface, Interface } from "readline/promises"; -import { stdin, stdout } from 'process'; +import { stdin, stdout } from "process"; import { exec } from "child_process"; import { promisify } from "util"; @@ -24,20 +24,20 @@ async function getFiles(dir: string): Promise { } function kebabToPascal(input: string): string { - return ('-' + input).replace(/-(\w)/g, (_, c) => c.toUpperCase()); + return ("-" + input).replace(/-(\w)/g, (_, c) => c.toUpperCase()); } async function reprompt(cb: () => Promise): Promise { for (; ;) { const result = await cb(); - if (typeof result !== 'undefined') { + if (typeof result !== "undefined") { return result; } } } export const runCommand = (command: string, cwd: string = process.cwd()) => { - const env = { ...process.env, FORCE_COLOR: process.stdout.isTTY ? '1' : '0' }; + const env = { ...process.env, FORCE_COLOR: process.stdout.isTTY ? "1" : "0" }; const thread = exec(command, { cwd, env }); return new Promise((resolve, reject) => { thread.stdout?.pipe(process.stdout); @@ -67,7 +67,7 @@ async function folderExists(path: string): Promise { await stat(path); return true; } catch (err: any) { - if (err.code === 'ENOENT') { + if (err.code === "ENOENT") { return false; } throw err; @@ -78,7 +78,7 @@ const execAsync = promisify(exec); export async function isGitInstalled(): Promise { try { - await execAsync('git --version'); + await execAsync("git --version"); return true; } catch { return false; @@ -86,13 +86,13 @@ export async function isGitInstalled(): Promise { } function detectPackageManager() { - const pkgManager = process.env.npm_config_user_agent?.split(' ')[0]?.split('/')[0] ?? 'npm'; + const pkgManager = process.env.npm_config_user_agent?.split(" ")[0]?.split("/")[0] ?? "npm"; switch (pkgManager) { - case 'yarn': return { name: 'yarn', install: 'yarn', run: 'yarn' } as const; - case 'pnpm': return { name: 'pnpm', install: 'pnpm install', run: 'pnpm run' } as const; - case 'bun': return { name: 'bun', install: 'bun install', run: 'bun run' } as const; - default: return { name: 'npm', install: 'npm install', run: 'npm run' } as const; + case "yarn": return { name: "yarn", install: "yarn", run: "yarn" } as const; + case "pnpm": return { name: "pnpm", install: "pnpm install", run: "pnpm run" } as const; + case "bun": return { name: "bun", install: "bun install", run: "bun run" } as const; + default: return { name: "npm", install: "npm install", run: "npm run" } as const; } } @@ -100,19 +100,30 @@ const reservedTactWords = [ "Int", "Bool", "Builder", "Slice", "Cell", "Address", "String", "StringBuilder", ]; +const templateNameRoots = { + "Empty contract": join(__dirname, "../template/empty"), + "Quick start": join(__dirname, "../template/quick-start"), +}; + async function main(reader: Interface) { - const templateRoot = join(__dirname, "../template/empty"); - - const packageName = await reprompt(async () => { - const placeholder = 'example'; - const fullPrompt = `Enter package name (${placeholder}): `; - const result = (await reader.question(fullPrompt)) || placeholder; - const validPackageName = /^(?=.{1,214}$)(?:@[a-z0-9]+(?:[._-][a-z0-9]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*$/; - if (!result.match(validPackageName)) { - return; - } - return result; - }); + let templateRoot = templateNameRoots["Quick start"]; + let packageName = "tact-quick-start"; + let contractName = "QuickStart"; + let isQuickStart = process.argv.slice(2).includes("--quick-start"); + + if (!isQuickStart) { + templateRoot = templateNameRoots["Empty contract"]; + packageName = await reprompt(async () => { + const placeholder = "example"; + const fullPrompt = `Enter package name (${placeholder}): `; + const result = (await reader.question(fullPrompt)) || placeholder; + const validPackageName = /^(?=.{1,214}$)(?:@[a-z0-9]+(?:[._-][a-z0-9]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*$/; + if (!result.match(validPackageName)) { + return; + } + return result; + }); + } const targetRoot = join(process.cwd(), packageName); @@ -137,16 +148,18 @@ async function main(reader: Interface) { // }); } - const contractName = await reprompt(async () => { - const placeholder = kebabToPascal(packageName); - const fullPrompt = `Enter contract name (${placeholder}): `; - const result = (await reader.question(fullPrompt)) || placeholder; - const validContractName = /[A-Z][a-zA-Z0-9_]*/; - if (!result.match(validContractName) || reservedTactWords.includes(result)) { - return; - } - return result; - }); + if (!isQuickStart) { + contractName = await reprompt(async () => { + const placeholder = kebabToPascal(packageName); + const fullPrompt = `Enter contract name (${placeholder}): `; + const result = (await reader.question(fullPrompt)) || placeholder; + const validContractName = /[A-Z][a-zA-Z0-9_]*/; + if (!result.match(validContractName) || reservedTactWords.includes(result)) { + return; + } + return result; + }); + } const manager = detectPackageManager(); @@ -194,12 +207,12 @@ async function main(reader: Interface) { process.exit(31); } } else { - console.error('Git is not installed'); - console.error('Git repository will not be initialized'); + console.error("Git is not installed"); + console.error("Git repository will not be initialized"); } - console.log('To switch to generated project, use'); - console.log(`cd ${relative(process.cwd(), targetRoot)}`) + console.log("To switch to generated project, use"); + console.log(`cd ${relative(process.cwd(), targetRoot)}`); } async function withReader(cb: (reader: Interface) => Promise): Promise { @@ -207,8 +220,8 @@ async function withReader(cb: (reader: Interface) => Promise): Promise try { return await cb(reader); } finally { - reader.close() + reader.close(); } } -void withReader(main); \ No newline at end of file +void withReader(main); diff --git a/template/empty/tact.config.json b/template/empty/tact.config.json index ee63dc5..6045288 100644 --- a/template/empty/tact.config.json +++ b/template/empty/tact.config.json @@ -1,4 +1,5 @@ { + "$schema": "http://raw.githubusercontent.com/tact-lang/tact/main/src/config/configSchema.json", "projects": [ { "name": "projectName", @@ -12,6 +13,12 @@ "interfacesGetter": false, "experimental": { "inline": false + }, + "safety": { + "nullChecks": true + }, + "optimizations": { + "internalExternalReceiversOutsideMethodsMap": false } } } diff --git a/template/quick-start/.github/workflows/pull.yml b/template/quick-start/.github/workflows/pull.yml new file mode 100644 index 0000000..679a7b9 --- /dev/null +++ b/template/quick-start/.github/workflows/pull.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + node-version: [22] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Enable corepack + run: corepack enable + + - name: Activate chosen package manager + run: corepack prepare --activate + + - name: Install dependencies + run: pmInstall + + - name: Build + run: pmRun build + + - name: Run Tact formatter + run: pmRun fmt:check + + - name: Run Misti + run: pmRun lint + + - name: Run tests + run: pmRun test diff --git a/template/quick-start/.prettierrc.json b/template/quick-start/.prettierrc.json new file mode 100644 index 0000000..611ddf9 --- /dev/null +++ b/template/quick-start/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "useTabs": false +} diff --git a/template/quick-start/.vscode/extensions.json b/template/quick-start/.vscode/extensions.json new file mode 100644 index 0000000..8759306 --- /dev/null +++ b/template/quick-start/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tonstudio.vscode-tact", "tonwhales.func-vscode", "dbaeumer.vscode-eslint"] +} diff --git a/template/quick-start/README.md b/template/quick-start/README.md new file mode 100644 index 0000000..c965fcd --- /dev/null +++ b/template/quick-start/README.md @@ -0,0 +1,36 @@ +# packageName + +## Project structure + +- `src/main.tact` – source code of contract +- `src/main.spec.ts` – test suite +- `deploy.ts` – script for deploying the contract +- `tact.config.json` – compiler settings + +## How to use + +- `pmRun build` – build `.ts` API for contract +- `pmRun test` – build contracts and run jest tests +- `pmRun fmt` – fix source code formatting +- `pmRun lint` – run semantic checks with `misti` linter +- `pmRun verifier:testnet` – deploy contract to testnet +- `pmRun verifier:mainnet` – deploy contract to mainnet +- `pmRun fmt:check` – check source code formatting (for CI) + +## Available CLIs + +- `tact` – Tact compiler +- `tact-fmt` – Tact formatter +- `unboc` – Disassembler +- `@nowarp/misti` – Misti static analyzer +- `jest` – Jest testing framework + +Use `npx` to run any of the CLIs available. For example, to invoke the Tact formatter, execute this: + +```shell +npx tact-fmt +``` + +## Learn more about Tact + +TODO. Perhaps, it'd be nice for some mini-tutorial here or in the contracts folder itself. diff --git a/template/quick-start/deploy.ts b/template/quick-start/deploy.ts new file mode 100644 index 0000000..9771df2 --- /dev/null +++ b/template/quick-start/deploy.ts @@ -0,0 +1,21 @@ +import { resolve } from "path"; +import { readFile } from "fs/promises"; +import { contractAddress } from "@ton/core"; +import { prepareTactDeployment } from "@tact-lang/deployer"; +import { ContractName } from "./output/projectName_ContractName"; + +async function main() { + console.log("Deploying..."); + const toProduction = process.argv.length === 3 && process.argv[2] === "mainnet"; + const init = await ContractName.init(); + const prepare = await prepareTactDeployment({ + pkg: await readFile(resolve(__dirname, "output", "projectName_ContractName.pkg")), + data: init.data.toBoc(), + testnet: !toProduction, + }); + const address = contractAddress(0, init).toString({ testOnly: !toProduction }); + console.log(`Contract address: ${address}`); + console.log(`Please, follow deployment link: ${prepare}`); +} + +void main(); diff --git a/template/quick-start/gitignore b/template/quick-start/gitignore new file mode 100644 index 0000000..2002ef4 --- /dev/null +++ b/template/quick-start/gitignore @@ -0,0 +1,7 @@ +node_modules +dist +output +.idea/ +.DS_Store/ +*.swp + diff --git a/template/quick-start/jest.config.ts b/template/quick-start/jest.config.ts new file mode 100644 index 0000000..809a7f9 --- /dev/null +++ b/template/quick-start/jest.config.ts @@ -0,0 +1,6 @@ +export default { + preset: "ts-jest", + testEnvironment: "node", + testPathIgnorePatterns: ["/node_modules/", "/output/", "/dist/"], + snapshotSerializers: ["@tact-lang/ton-jest/serializers"], +}; diff --git a/template/quick-start/package.json b/template/quick-start/package.json new file mode 100644 index 0000000..d7ff9bc --- /dev/null +++ b/template/quick-start/package.json @@ -0,0 +1,31 @@ +{ + "name": "packageName", + "version": "0.0.0", + "scripts": { + "build": "tact --config ./tact.config.json", + "fmt": "tact-fmt --write ./src && prettier -l -w .", + "fmt:check": "tact-fmt --check ./src && prettier --check .", + "lint": "misti ./tact.config.json", + "test": "pmRun build && jest", + "verifier:testnet": "pmRun test && ts-node deploy.ts", + "verifier:mainnet": "pmRun test && ts-node deploy.ts mainnet" + }, + "devDependencies": { + "@nowarp/misti": "~0.8.1", + "@tact-lang/compiler": "^1.6.6", + "@tact-lang/deployer": "^0.2.0", + "@tact-lang/ton-jest": "^0.0.4", + "@ton/core": "^0.60.1", + "@ton/crypto": "^3.3.0", + "@ton/sandbox": "^0.28.0", + "@ton/test-utils": "^0.5.0", + "@ton/ton": "^15.2.1", + "@types/jest": "^29.5.14", + "@types/node": "^22.14.0", + "jest": "^29.7.0", + "prettier": "^3.5.3", + "ts-jest": "^29.3.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/template/quick-start/src/counter.spec.ts b/template/quick-start/src/counter.spec.ts new file mode 100644 index 0000000..697b7a3 --- /dev/null +++ b/template/quick-start/src/counter.spec.ts @@ -0,0 +1,22 @@ +import "@ton/test-utils"; +import { toNano } from "@ton/core"; +import { Blockchain } from "@ton/sandbox"; +import { Counter } from "../output/counter_Counter"; + +it("should deploy correctly", async () => { + const blockchain = await Blockchain.create(); + + const contract = blockchain.openContract(await Counter.fromInit()); + + const deployer = await blockchain.treasury("deployer"); + + // Send a message that `receive()` would handle + const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: contract.address, + deploy: true, + success: true, + }); +}); diff --git a/template/quick-start/src/counter.tact b/template/quick-start/src/counter.tact new file mode 100644 index 0000000..bef748f --- /dev/null +++ b/template/quick-start/src/counter.tact @@ -0,0 +1,38 @@ +/// TODO: A proper welcome! +/// +contract Counter( + // Persistent state variables declared via the + // contract parameters syntax, which was introduced in v1.6.0 + // + // See: https://docs.tact-lang.org/book/contracts/#parameters + id: Int as uint32, + counter: Int as uint32, +) { + // Empty receiver for the deployment, + // which expects the `null` message body + receive() { + // Forward the remaining value in the + // incoming message back to the sender + cashback(sender()); + } + + receive(msg: Add) { + self.counter += msg.amount; + + // Forward the remaining value in the + // incoming message back to the sender + cashback(sender()); + } + + get fun counter(): Int { + return self.counter; + } + + get fun id(): Int { + return self.id; + } +} + +message Add { + amount: Int as uint32; +} diff --git a/template/quick-start/src/poll.spec.ts b/template/quick-start/src/poll.spec.ts new file mode 100644 index 0000000..4114bf6 --- /dev/null +++ b/template/quick-start/src/poll.spec.ts @@ -0,0 +1,22 @@ +import "@ton/test-utils"; +import { toNano } from "@ton/core"; +import { Blockchain } from "@ton/sandbox"; +import { Poll } from "../output/poll_Poll"; + +it("should deploy correctly", async () => { + const blockchain = await Blockchain.create(); + + const contract = blockchain.openContract(await Poll.fromInit()); + + const deployer = await blockchain.treasury("deployer"); + + // Send a message that `receive()` would handle + const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); + + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: contract.address, + deploy: true, + success: true, + }); +}); diff --git a/template/quick-start/src/poll.tact b/template/quick-start/src/poll.tact new file mode 100644 index 0000000..be3f980 --- /dev/null +++ b/template/quick-start/src/poll.tact @@ -0,0 +1,70 @@ +contract Poll( + _randomId: Int as uint256, + option0: Int as uint32, + option1: Int as uint32, +) { + // Empty receiver for the deployment, + // which expects the `null` message body + receive() { + // Forward the remaining value in the + // incoming message back to the sender + cashback(sender()); + } + + // TODO: Describe + receive(msg: Vote) { + require(msg.option >= 0 && msg.option <= 1, "Only options allowed are: 0 and 1"); + + // TODO: Give proper explanation. + // Unconditionally process the vote + // The check for whether the given voter has voted already will be done later + if (msg.option == 0) { + self.option0 += 1; + } else { + self.option1 += 1; + } + + // Send a message to existing Voter contract, + // which will deploy it if it didn't exist yet + deploy(DeployParameters { + value: 0, + mode: SendRemainingValue, + init: initOf Voter(sender(), myAddress(), false), + }); + } + + // TODO: Describe + // + // We've received it because... + bounced(msg: bounced) { + if (msg.option == 0) { + self.option0 -= 1; + } else { + self.option1 -= 1; + } + } +} + +contract Voter( + _origin: Address, + poll: Address, + hasVoted: Bool, +) { + receive() { + require( + self.poll == sender(), + "New votes are accepted only from the parent Poll contract", + ); + require(!self.hasVoted, "This voter has participated already"); + self.hasVoted = true; + } + + get fun hasVoted(): Bool { + return self.hasVoted; + } +} + +message Vote { + // Allows only 0 or 1 + option: Int as uint1; +} diff --git a/template/quick-start/tact.config.json b/template/quick-start/tact.config.json new file mode 100644 index 0000000..94eaba3 --- /dev/null +++ b/template/quick-start/tact.config.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://raw.githubusercontent.com/tact-lang/tact/main/src/config/configSchema.json", + "projects": [ + { + "name": "counter", + "path": "./src/counter.tact", + "output": "./output", + "mode": "full", + "options": { + "external": false, + "debug": true, + "ipfsAbiGetter": false, + "interfacesGetter": false, + "experimental": { + "inline": false + }, + "safety": { + "nullChecks": true + }, + "optimizations": { + "internalExternalReceiversOutsideMethodsMap": false + } + } + }, + { + "name": "poll", + "path": "./src/poll.tact", + "output": "./output", + "mode": "full", + "options": { + "external": false, + "debug": true, + "ipfsAbiGetter": false, + "interfacesGetter": false, + "experimental": { + "inline": false + } + } + } + ] +} diff --git a/template/quick-start/tsconfig.json b/template/quick-start/tsconfig.json new file mode 100644 index 0000000..465597f --- /dev/null +++ b/template/quick-start/tsconfig.json @@ -0,0 +1,65 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "outDir": "./dist", + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "lib": ["ESNext"], + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": false, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "resolveJsonModule": true + } +} From d535ec1d3b5c6b307c4eaf8fb7fe5cc064410be7 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Tue, 6 May 2025 09:31:25 +0200 Subject: [PATCH 02/10] feat: deploy script and counter contract --- template/quick-start/deploy.ts | 51 ++++++++++++++---- template/quick-start/src/counter.tact | 75 +++++++++++++++++++++------ 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/template/quick-start/deploy.ts b/template/quick-start/deploy.ts index 9771df2..7ceb511 100644 --- a/template/quick-start/deploy.ts +++ b/template/quick-start/deploy.ts @@ -2,20 +2,51 @@ import { resolve } from "path"; import { readFile } from "fs/promises"; import { contractAddress } from "@ton/core"; import { prepareTactDeployment } from "@tact-lang/deployer"; -import { ContractName } from "./output/projectName_ContractName"; +import { Counter } from "./output/counter_Counter"; +import { Poll } from "./output/poll_Poll"; async function main() { console.log("Deploying..."); const toProduction = process.argv.length === 3 && process.argv[2] === "mainnet"; - const init = await ContractName.init(); - const prepare = await prepareTactDeployment({ - pkg: await readFile(resolve(__dirname, "output", "projectName_ContractName.pkg")), - data: init.data.toBoc(), - testnet: !toProduction, - }); - const address = contractAddress(0, init).toString({ testOnly: !toProduction }); - console.log(`Contract address: ${address}`); - console.log(`Please, follow deployment link: ${prepare}`); + + // 1. Counter contract + { + // Default (initial) values of persistent state variables are supplied here + const init = await Counter.init(0n, 0n); + + // Obtaining a convenient link to deploy a new contract in the mainnet or testnet, + // which could be used by your existing Toncoin wallet. + const prepare = await prepareTactDeployment({ + // The .pkg file is a special JSON file containing the compiled contract, + // its dependencies, and all related metadata. + // + // See: https://docs.tact-lang.org/ref/evolution/otp-006/ + pkg: await readFile(resolve(__dirname, "output", "counter_Counter.pkg")), + data: init.data.toBoc(), + testnet: !toProduction, + }); + + // Contract addresses on TON are obtained deterministically, + // from the initial code and data. The most used chain is basechain, + // which is the workchain with ID 0. + const address = contractAddress(0, init).toString({ testOnly: !toProduction }); + console.log(`Contract address: ${address}`); + console.log(`Please, follow deployment link: ${prepare}`); + } + + // 2. Poll contract — the procedure is the same, + // but persistent state variables and contract output would differ. + { + const init = await Poll.init(0n, 0n, 0n); + const prepare = await prepareTactDeployment({ + pkg: await readFile(resolve(__dirname, "output", "poll_Poll.pkg")), + data: init.data.toBoc(), + testnet: !toProduction, + }); + const address = contractAddress(0, init).toString({ testOnly: !toProduction }); + console.log(`Contract address: ${address}`); + console.log(`Please, follow deployment link: ${prepare}`); + } } void main(); diff --git a/template/quick-start/src/counter.tact b/template/quick-start/src/counter.tact index bef748f..3176a6d 100644 --- a/template/quick-start/src/counter.tact +++ b/template/quick-start/src/counter.tact @@ -1,38 +1,79 @@ -/// TODO: A proper welcome! -/// +//////////////////////////////////////////////////////////////// +// // +// Welcome to your first Tact smart contract! // +// ------------------------------------------ // +// // +// ;@ // +// 88; ,d ,d // +// 888: 88 88 // +// %88%8888X; MM88MMM ,adPPYYba, ,adPPYba, MM88MMM // +// ;@8888@8XX@ 88 "" `Y8 a8" "" 88 // +// 8X88 88 ,adPPPPP88 8b 88 // +// 888 88, 88, ,88 "8a, ,aa 88, // +// S88 "Y888 `"8bbdP"Y8 `"Ybbd8"' "Y888 // +// t8; // +// // +//////////////////////////////////////////////////////////////// + +/// Let's define a simple counter contract. +/// Comments below describe each of its parts. contract Counter( - // Persistent state variables declared via the - // contract parameters syntax, which was introduced in v1.6.0 + // The following are persistent state variables of the contract. + // + // Their default (initial) values are supplied during deployment + // and the current values are kept on the Blockchain, + // persisting between transactions. // - // See: https://docs.tact-lang.org/book/contracts/#parameters - id: Int as uint32, - counter: Int as uint32, + counter: Int as uint32, // actual value of the counter + id: Int as uint32, // a unique ID to deploy multiple instances + // of this contract in a same workchain ) { - // Empty receiver for the deployment, - // which expects the `null` message body + /// Registers a receiver of empty messages from other contracts. + /// + /// It handles internal messages with `null` body + /// and is very handy and cheap for the deployments. + /// + /// To deploy this contract in the local TON emulator, Sandbox, + /// we'll send a `null` body message from the special "treasury" contract + /// to the address of this one. See the `deploy.ts` script for more. receive() { // Forward the remaining value in the - // incoming message back to the sender + // incoming message back to the sender. cashback(sender()); } - receive(msg: Add) { - self.counter += msg.amount; + // Registers a binary receiver of the Add message bodies. + // + // Notice that we do not use the received body + // and it is discarded with a wildcard _ + receive(_: Add) { + // Duh + self.counter += 1; // Forward the remaining value in the // incoming message back to the sender cashback(sender()); } + /// A getter function, which can only be called from off-chain + /// and never by other contracts. This one is useful to see the counter state. get fun counter(): Int { - return self.counter; + return self.counter; // ← current counter value in the contract's storage } + /// Another getter function, now for the ID. get fun id(): Int { - return self.id; + return self.id; // ← current ID value in the contract's storage (persistent state) } -} -message Add { - amount: Int as uint32; + /// Notice that you can make a single getter that returns the contract's state + /// with all its storage variables at once by using a special structure + /// with the name of the given contract. + get fun state(): Counter { + return self; + } } + +// Defining a new message struct type, which has one field +// and an automatically assigned 32-bit opcode prefix +message Add {} From 6b014591dc54a294e2d0da5844de71be7e86322d Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 8 May 2025 16:23:08 +0200 Subject: [PATCH 03/10] upd --- template/empty/src/main.spec.ts | 4 +- template/quick-start/src/counter.spec.ts | 67 +++++++++++++++++++----- template/quick-start/src/counter.tact | 7 ++- template/quick-start/src/poll.spec.ts | 50 +++++++++++++----- 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/template/empty/src/main.spec.ts b/template/empty/src/main.spec.ts index 4da0882..8117c98 100644 --- a/template/empty/src/main.spec.ts +++ b/template/empty/src/main.spec.ts @@ -5,12 +5,10 @@ import { ContractName } from "../output/projectName_ContractName"; it("should deploy correctly", async () => { const blockchain = await Blockchain.create(); - const contract = blockchain.openContract(await ContractName.fromInit()); - const deployer = await blockchain.treasury("deployer"); - // call `receive()` + // Send a message that `receive()` would handle const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); expect(result.transactions).toHaveTransaction({ diff --git a/template/quick-start/src/counter.spec.ts b/template/quick-start/src/counter.spec.ts index 697b7a3..33ce395 100644 --- a/template/quick-start/src/counter.spec.ts +++ b/template/quick-start/src/counter.spec.ts @@ -1,22 +1,65 @@ import "@ton/test-utils"; import { toNano } from "@ton/core"; -import { Blockchain } from "@ton/sandbox"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { Counter } from "../output/counter_Counter"; -it("should deploy correctly", async () => { - const blockchain = await Blockchain.create(); +describe("Counter contract", () => { + let blockchain: Blockchain; + let treasury: SandboxContract; - const contract = blockchain.openContract(await Counter.fromInit()); + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasury = await blockchain.treasury("deployer"); + }); - const deployer = await blockchain.treasury("deployer"); + it("should be deployed correctly", async () => { + // Prepare a contract wrapper with initial data given as 0n, 0n: + // counter and ID both set to 0 + const contract = blockchain.openContract(await Counter.fromInit(0n, 0n)); - // Send a message that `receive()` would handle - const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); + // Send a message that `receive()` would handle + const sendResult = await contract.send(treasury.getSender(), { value: toNano(1) }, null); - expect(result.transactions).toHaveTransaction({ - from: deployer.address, - to: contract.address, - deploy: true, - success: true, + // Expect a successful deployment + expect(sendResult.transactions).toHaveTransaction({ + from: treasury.address, + to: contract.address, + deploy: true, + success: true, + }); }); + + it("should increase the counter", async () => { + const contract = blockchain.openContract(await Counter.fromInit(0n, 0n)); + let sendResult = await contract.send(treasury.getSender(), { value: toNano(1) }, null); + expect(sendResult.transactions).toHaveTransaction({ + from: treasury.address, + to: contract.address, + deploy: true, + success: true, + }); + + // Let's increase the counter 3 times + let incTimes = 3; + while (incTimes--) { + // And each time check the counter state before and after the Add message received + const counterBefore = await contract.getCounter(); + + // The message should be sent and received successfully + sendResult = await contract.send(treasury.getSender(), { value: toNano(1) }, { $$type: 'Add' }); + expect(sendResult.transactions).toHaveTransaction({ + from: treasury.address, + to: contract.address, + success: true, + }); + + // The counter's state after receiving the Add message should increase by 1 + const counterAfter = await contract.getCounter(); + expect(counterAfter - counterBefore).toBe(1n); + } + }); + + // NOTE: To add your own tests, simply copy-paste any of the `it()` clauses + // and adjust the logic to match your expected outcomes! }); diff --git a/template/quick-start/src/counter.tact b/template/quick-start/src/counter.tact index 3176a6d..4290adf 100644 --- a/template/quick-start/src/counter.tact +++ b/template/quick-start/src/counter.tact @@ -74,6 +74,9 @@ contract Counter( } } -// Defining a new message struct type, which has one field -// and an automatically assigned 32-bit opcode prefix +// Defining a new message struct type, which has no fields, +// but it has an automatically assigned 32-bit opcode prefix. message Add {} + +// If you've got your grip on the Counter contract, +// let's move to the second one: Open-ended Poll (poll.tact) diff --git a/template/quick-start/src/poll.spec.ts b/template/quick-start/src/poll.spec.ts index 4114bf6..a0f90c6 100644 --- a/template/quick-start/src/poll.spec.ts +++ b/template/quick-start/src/poll.spec.ts @@ -1,22 +1,48 @@ import "@ton/test-utils"; import { toNano } from "@ton/core"; -import { Blockchain } from "@ton/sandbox"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; import { Poll } from "../output/poll_Poll"; -it("should deploy correctly", async () => { - const blockchain = await Blockchain.create(); +describe("Poll contract", () => { + let blockchain: Blockchain; + let treasury: SandboxContract; - const contract = blockchain.openContract(await Poll.fromInit()); + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasury = await blockchain.treasury("deployer"); + }); - const deployer = await blockchain.treasury("deployer"); + it("should be deployed correctly", async () => { + // Prepare a contract wrapper with initial data given as 0n, 0n, 0n: + // ID, option0, and option1 all set to 0 + const contract = blockchain.openContract(await Poll.fromInit(0n, 0n, 0n)); - // Send a message that `receive()` would handle - const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); + // Send a message that `receive()` would handle + const sendResult = await contract.send(treasury.getSender(), { value: toNano(1) }, null); - expect(result.transactions).toHaveTransaction({ - from: deployer.address, - to: contract.address, - deploy: true, - success: true, + // Expect a successful deployment + expect(sendResult.transactions).toHaveTransaction({ + from: treasury.address, + to: contract.address, + deploy: true, + success: true, + }); }); + + it("should deploy Voter contracts correctly", async () => { + const contract = blockchain.openContract(await Poll.fromInit(0n, 0n, 0n)); + let sendResult = await contract.send(treasury.getSender(), { value: toNano(1) }, null); + expect(sendResult.transactions).toHaveTransaction({ + from: treasury.address, + to: contract.address, + deploy: true, + success: true, + }); + + // ... + }); + + // NOTE: To add your own tests, simply copy-paste any of the `it()` clauses + // and adjust the logic to match your expected outcomes! }); From cb41a7d8a0dc22656b0faa4f4d1d489feff06080 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 9 May 2025 12:13:02 +0200 Subject: [PATCH 04/10] test --- template/quick-start/src/poll.spec.ts | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/template/quick-start/src/poll.spec.ts b/template/quick-start/src/poll.spec.ts index a0f90c6..3fd1c6c 100644 --- a/template/quick-start/src/poll.spec.ts +++ b/template/quick-start/src/poll.spec.ts @@ -40,7 +40,45 @@ describe("Poll contract", () => { success: true, }); - // ... + // Let's create Toncoin wallets for two voters + const wallet0 = await blockchain.treasury("voter-wallet-0"); + const wallet1 = await blockchain.treasury("voter-wallet-1"); + + // And a small helper function for our needs + const voteFrom = async ( + wallet: SandboxContract, + option: 0n | 1n, + success = true, + ) => { + const res = await contract.send(wallet.getSender(), { value: toNano(1) }, {$$type: 'Vote', option }); + expect(res.transactions).toHaveTransaction({ + from: wallet.address, + to: contract.address, + success, + }); + }; + + // Vote for option 0 from the first voter's wallet + await voteFrom(wallet0, 0n); + + // Vote for option 1 from the second voter's wallet + await voteFrom(wallet1, 1n); + + // There should be a vote for option one and option two + const state = await contract.getState(); + expect(state.option0).toBe(1n); + expect(state.option1).toBe(1n); + + // Finally, voters should not be able to vote more than once. + // That is, the resulting state after new vote attempts should not change. + await voteFrom(wallet0, 0n); // repeat previous vote + await voteFrom(wallet0, 1n); // attempt a different vote + await voteFrom(wallet1, 1n); // repeat previous vote + await voteFrom(wallet1, 0n); // attempt a different vote + + const finalState = await contract.getState(); + expect(finalState.option0).toBe(state.option0); + expect(finalState.option1).toBe(state.option1); }); // NOTE: To add your own tests, simply copy-paste any of the `it()` clauses From bc7f74e34d0e8daca150eed68188f92f3ca44dab Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 9 May 2025 15:01:19 +0200 Subject: [PATCH 05/10] almost there --- src/index.ts | 7 +++- template/empty/README.md | 14 +++++++ template/empty/gitignore | 4 +- template/quick-start/gitignore | 4 +- template/quick-start/src/counter.tact | 22 +++++++---- template/quick-start/src/poll.tact | 56 ++++++++++++++++++++++----- 6 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index bdcdaec..687858e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -211,8 +211,13 @@ async function main(reader: Interface) { console.error("Git repository will not be initialized"); } + const relDir = relative(process.cwd(), targetRoot); console.log("To switch to generated project, use"); - console.log(`cd ${relative(process.cwd(), targetRoot)}`); + console.log(`cd ${relDir}`); + + if (isQuickStart) { + console.log(`\n→ See the ${relDir}/README.md to get started!`); + } } async function withReader(cb: (reader: Interface) => Promise): Promise { diff --git a/template/empty/README.md b/template/empty/README.md index 7674c56..379d24e 100644 --- a/template/empty/README.md +++ b/template/empty/README.md @@ -16,3 +16,17 @@ - `pmRun verifier:testnet` – deploy contract to testnet - `pmRun verifier:mainnet` – deploy contract to mainnet - `pmRun fmt:check` – check source code formatting (for CI) + +## Available CLIs + +- `tact` – Tact compiler +- `tact-fmt` – Tact formatter +- `unboc` – Disassembler +- `@nowarp/misti` – Misti static analyzer +- `jest` – Jest testing framework + +Use `npx` to run any of the CLIs available. For example, to invoke the Tact formatter, execute this: + +```shell +npx tact-fmt +``` diff --git a/template/empty/gitignore b/template/empty/gitignore index 2002ef4..b456ead 100644 --- a/template/empty/gitignore +++ b/template/empty/gitignore @@ -4,4 +4,6 @@ output .idea/ .DS_Store/ *.swp - +.helix/ +.vim/ +.nvim/ diff --git a/template/quick-start/gitignore b/template/quick-start/gitignore index 2002ef4..b456ead 100644 --- a/template/quick-start/gitignore +++ b/template/quick-start/gitignore @@ -4,4 +4,6 @@ output .idea/ .DS_Store/ *.swp - +.helix/ +.vim/ +.nvim/ diff --git a/template/quick-start/src/counter.tact b/template/quick-start/src/counter.tact index 4290adf..5262f28 100644 --- a/template/quick-start/src/counter.tact +++ b/template/quick-start/src/counter.tact @@ -42,10 +42,10 @@ contract Counter( cashback(sender()); } - // Registers a binary receiver of the Add message bodies. - // - // Notice that we do not use the received body - // and it is discarded with a wildcard _ + /// Registers a binary receiver of the Add message bodies. + /// + /// Notice that we do not use the received body + /// and it is discarded with a wildcard _ receive(_: Add) { // Duh self.counter += 1; @@ -74,9 +74,17 @@ contract Counter( } } -// Defining a new message struct type, which has no fields, -// but it has an automatically assigned 32-bit opcode prefix. -message Add {} +/// Defining a new message struct type, +/// which has no fields and a manually assigned 32-bit opcode prefix. +/// +/// The opcode goes before all message body fields and serves as a tag, +/// helping contracts differentiate the message bodies and parse their subsequent +/// contents in accordance to their message struct layouts. +/// +/// In our case there are no fields after the opcode as the message struct is empty, +/// but in the real-world contracts there are about 4-5 fields on average +/// in each defined message struct. +message(0x12345678) Add {} // If you've got your grip on the Counter contract, // let's move to the second one: Open-ended Poll (poll.tact) diff --git a/template/quick-start/src/poll.tact b/template/quick-start/src/poll.tact index be3f980..fe17315 100644 --- a/template/quick-start/src/poll.tact +++ b/template/quick-start/src/poll.tact @@ -1,3 +1,15 @@ +/// The parent Poll contract deploys individual Voter contracts for each voter's Toncoin wallet. +/// This approach for building contract systems is called "sharding", and it is very common on TON. +/// Instead of attempting to make a single large contract process everything, +/// it is much better to deploy small contracts for each user +/// and have a single main (or master) contract provisioning those small contracts. +/// +/// Sharding is the best approach on TON for many reasons: +/// * The maximum state size of each contract is very limited +/// * Contracts can only send and receive messages to interact with each other, +/// which makes all communications asynchronous +/// * The network itself can shard and process multiple contracts at a time +/// contract Poll( _randomId: Int as uint256, option0: Int as uint32, @@ -11,13 +23,21 @@ contract Poll( cashback(sender()); } - // TODO: Describe + /// Registers a binary receiver of the Vote message bodies. + /// + /// Notice that we do not use the received body + /// and it is discarded with a wildcard _ receive(msg: Vote) { - require(msg.option >= 0 && msg.option <= 1, "Only options allowed are: 0 and 1"); + // Limit available options. + require( + msg.option >= 0 && msg.option <= 1, + "Only options allowed are: 0 and 1", + ); - // TODO: Give proper explanation. - // Unconditionally process the vote - // The check for whether the given voter has voted already will be done later + // Unconditionally process the vote. + // Notice that the checks for duplicated votes is done in the Voter contract + // to reduce the overall gas fees and simplify the checking logic + // as we cannot query other contracts syncronously on TON. if (msg.option == 0) { self.option0 += 1; } else { @@ -25,11 +45,13 @@ contract Poll( } // Send a message to existing Voter contract, - // which will deploy it if it didn't exist yet + // which will deploy it if it didn't exist yet. deploy(DeployParameters { value: 0, - mode: SendRemainingValue, - init: initOf Voter(sender(), myAddress(), false), + mode: SendRemainingValue, // use the excesses in the received message + init: initOf Voter(sender(), myAddress(), false), // pass initial data + body: inMsg().asCell(), // make sure the Vote message is sent + // This is important for the remaining logic to work. }); } @@ -43,6 +65,13 @@ contract Poll( self.option1 -= 1; } } + + /// As we've seen in the Counter contract, you can make a single getter that returns + /// the contract's state with all its storage variables at once by using + /// a special structure with the name of the given contract. + get fun state(): Poll { + return self; + } } contract Voter( @@ -50,7 +79,12 @@ contract Voter( poll: Address, hasVoted: Bool, ) { - receive() { + /// Registers a binary receiver of the Vote message bodies. + /// Since its the only defined receiver, it will be used for the deployment. + /// + /// Notice that we do not use the received body + /// and it is discarded with a wildcard _ + receive(_: Vote) { require( self.poll == sender(), "New votes are accepted only from the parent Poll contract", @@ -64,7 +98,9 @@ contract Voter( } } +/// Defining a new message struct type, +/// which has a single `option` field and an automatically assigned 32-bit opcode prefix. message Vote { - // Allows only 0 or 1 + /// Allows only 0 or 1 option: Int as uint1; } From fa73e96a99350ed45517eac3073138feec368b77 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 9 May 2025 15:17:07 +0200 Subject: [PATCH 06/10] readme, part 1 --- template/quick-start/README.md | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/template/quick-start/README.md b/template/quick-start/README.md index c965fcd..6d4dfcc 100644 --- a/template/quick-start/README.md +++ b/template/quick-start/README.md @@ -1,15 +1,39 @@ -# packageName +# Tact Quick Start + +Tact is a fresh programming language for TON Blockchain, focused on efficiency and ease of development. It is a good fit for complex smart contracts, quick onboarding and rapid prototyping. + +This quick start guide will help you [set up your editor or IDE](#editor-setup), understand some of the basics of Tact and TON, and learn from the given Tact smart contracts: a counter and an open-ended poll. + +We'll also cover how to create, test, and deploy contracts using the provided template, which includes a complete development environment with all the necessary tools pre-configured. + +## Editor setup + +- [VS Code extension](https://marketplace.visualstudio.com/items?itemName=tonstudio.vscode-tact) - Powerful and feature-rich extension for Visual Studio Code (VSCode) and VSCode-based editors like VSCodium, Cursor, Windsurf, and others. + - Get it on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=tonstudio.vscode-tact). + - Get it on the [Open VSX Registry](https://open-vsx.org/extension/tonstudio/vscode-tact). + - Or install from the [`.vsix` files in nightly releases](https://github.com/tact-lang/tact-language-server/releases). +- [Language Server (LSP Server)](https://github.com/tact-lang/tact-language-server) - Supports Sublime Text, (Neo)Vim, Helix, and other editors with LSP support. +- [Intelli Tact](https://plugins.jetbrains.com/plugin/27290-tact) - Powerful and feature-rich plugin for JetBrains IDEs like WebStorm, IntelliJ IDEA, and others. +- [tact.vim](https://github.com/tact-lang/tact.vim) - Vim 8+ plugin. +- [tact-sublime](https://github.com/tact-lang/tact-sublime) - Sublime Text 4 package. + - Get it on the [Package Control](https://packagecontrol.io/packages/Tact). +- [tree-sitter-tact](https://github.com/tact-lang/tree-sitter-tact) - Tree-sitter grammar for Tact. + +The language server, VSCode extension, and JetBrains plugin linked above come with hover documentation for built-in keywords, which is very helpful for discovering the language on your own! + +If you're done with the setup, let's discuss the contents of this project. ## Project structure - `src/main.tact` – source code of contract - `src/main.spec.ts` – test suite -- `deploy.ts` – script for deploying the contract +- `deploy.ts` – script for deploying the contracts - `tact.config.json` – compiler settings -## How to use -- `pmRun build` – build `.ts` API for contract +## Commands + +- `pmRun build` – build contracts and the `.ts` wrappers - `pmRun test` – build contracts and run jest tests - `pmRun fmt` – fix source code formatting - `pmRun lint` – run semantic checks with `misti` linter @@ -33,4 +57,24 @@ npx tact-fmt ## Learn more about Tact -TODO. Perhaps, it'd be nice for some mini-tutorial here or in the contracts folder itself. +- [Website](https://tact-lang.org/) +- [Documentation](https://docs.tact-lang.org/) + - [Learn Tact in Y minutes](https://docs.tact-lang.org/book/learn-tact-in-y-minutes/) + - [Debugging and testing Tact contracts](https://docs.tact-lang.org/book/debug/) + - [Gas best practices](https://docs.tact-lang.org/book/gas-best-practices/) + - [Security best practices](https://docs.tact-lang.org/book/security-best-practices/) +- [Awesome Tact](https://github.com/tact-lang/awesome-tact) + +## Community + +If you can’t find the answer in the [docs](https://docs.tact-lang.org), or you’ve tried to do some local testing and it still didn’t help — don’t hesitate to reach out to Tact’s flourishing community: + +- [`@tactlang` on Telegram](https://t.me/tactlang) - Main community chat and discussion group. +- [`@tactlang_ru` on Telegram](https://t.me/tactlang_ru) _(Russian)_ +- [`@tact_kitchen` on Telegram](https://t.me/tact_kitchen) - Channel with updates from the team. +- [`@tact_language` on X/Twitter](https://x.com/tact_language) +- [`tact-lang` organization on GitHub](https://github.com/tact-lang) +- [`@ton_studio` on Telegram](https://t.me/ton_studio) +- [`@thetonstudio` on X/Twitter](https://x.com/thetonstudio) + +Good luck on your coding adventure with ⚡ Tact! From 4d386fd484fda3584f79926f169e7cd6195eb473 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 12 May 2025 03:51:50 +0200 Subject: [PATCH 07/10] readme, part 2 --- src/index.ts | 6 +++--- template/empty/README.md | 2 +- template/quick-start/README.md | 17 +++++++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 687858e..fb5b594 100644 --- a/src/index.ts +++ b/src/index.ts @@ -212,11 +212,11 @@ async function main(reader: Interface) { } const relDir = relative(process.cwd(), targetRoot); - console.log("To switch to generated project, use"); - console.log(`cd ${relDir}`); + console.log("\nTo switch to the generated project, use\n"); + console.error(` cd ${relDir}`); if (isQuickStart) { - console.log(`\n→ See the ${relDir}/README.md to get started!`); + console.log(`\nThen see the ${relDir}/README.md to get started!`); } } diff --git a/template/empty/README.md b/template/empty/README.md index 379d24e..398fb2c 100644 --- a/template/empty/README.md +++ b/template/empty/README.md @@ -9,7 +9,7 @@ ## How to use -- `pmRun build` – build `.ts` API for contract +- `pmRun build` – build contracts and the `.ts` wrappers - `pmRun test` – build contracts and run jest tests - `pmRun fmt` – fix source code formatting - `pmRun lint` – run semantic checks with `misti` linter diff --git a/template/quick-start/README.md b/template/quick-start/README.md index 6d4dfcc..a8048d2 100644 --- a/template/quick-start/README.md +++ b/template/quick-start/README.md @@ -25,11 +25,24 @@ If you're done with the setup, let's discuss the contents of this project. ## Project structure -- `src/main.tact` – source code of contract -- `src/main.spec.ts` – test suite +Start exploring this project with a Counter contract. Once ready, move to the Poll or to any of the auxiliary files. + +- `src/counter.tact` — source code of the Counter contract +- `src/counter.spec.ts` — test suite for it +- `src/poll.tact` — source code of the Open-ended Poll contract +- `src/poll.spec.ts` — test suite for it - `deploy.ts` – script for deploying the contracts - `tact.config.json` – compiler settings +To add new contract files to the project, do: + +1. Create a new `src/my-new-contract.tact` +2. Copy an existing test suite into the one for the new contract in `src/my-new-contract.spec.ts`, then adjust the contents to match the new contract +3. If the contract is used directly and not deployed by other ones, then: + * Modify `deploy.ts` + * Modify `tact.config.json` + +To build, test or deploy contracts see the following commands. ## Commands From b41dc3b082429a30e1cf1f4c8b8fa0665ac7d4ab Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 12 May 2025 04:09:55 +0200 Subject: [PATCH 08/10] resolve todos --- template/quick-start/src/poll.tact | 43 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/template/quick-start/src/poll.tact b/template/quick-start/src/poll.tact index fe17315..ce50928 100644 --- a/template/quick-start/src/poll.tact +++ b/template/quick-start/src/poll.tact @@ -1,3 +1,8 @@ +/// The following contract is similar to a poll feature you've probably seen on many +/// social media networks out there, except that now it works on the blockchain +/// and is decentralized — anyone can deploy their own poll and count unique votes +/// for some options. In this example, the poll only has 2 options to pick from. +/// /// The parent Poll contract deploys individual Voter contracts for each voter's Toncoin wallet. /// This approach for building contract systems is called "sharding", and it is very common on TON. /// Instead of attempting to make a single large contract process everything, @@ -23,7 +28,7 @@ contract Poll( cashback(sender()); } - /// Registers a binary receiver of the Vote message bodies. + /// Registers a binary receiver of the `Vote` message bodies. /// /// Notice that we do not use the received body /// and it is discarded with a wildcard _ @@ -55,9 +60,14 @@ contract Poll( }); } - // TODO: Describe - // - // We've received it because... + /// This is a special receiver — it only handles the `Vote` message bodies + /// that have bounced back to this contract, which can only happen if this contract + /// has sent a message and there was an error during its processing on the recepients' side. + /// + /// In our case, the receiver of the regular `Vote` message bodies sends a deployment message + /// with the `Vote` body, processing of which can intentionally cause an error if the voter + /// has voted already. And that would mean we need to revert the excessive vote change + /// applied earlier in the previous receiver. bounced(msg: bounced) { if (msg.option == 0) { self.option0 -= 1; @@ -74,14 +84,31 @@ contract Poll( } } +/// Child contract for each of the voter's Toncoin wallet. +/// By creating an individual contract we circumvent the problem of the limited contract's state +/// and effectively use the whole blockchain as a map of voters that did participate in a given poll. +/// +/// These contracts are not deployed on their own and isn't listed as an entrypoint in tact.config.json. +/// Instead, they are deployed by the parent Poll contract. +/// +/// Since contract addresses on TON are determined on deployment from the initial code and data, +/// it is crucial for the Voter contract to have some data that will be unique to each voter. +/// That is why there is a seemingly unused `_origin` address stored in the contract's persistent state. contract Voter( + /// An address of the voter's Toncoin wallet. _origin: Address, + + /// Address of the `Poll` contract this Voter will participate in. poll: Address, + + /// Whether this voter voted already. + /// This field is needed to only process unique votes + /// and it should be set to true upon the deployment of this contract. hasVoted: Bool, ) { - /// Registers a binary receiver of the Vote message bodies. + /// Registers a binary receiver of the `Vote` message bodies. /// Since its the only defined receiver, it will be used for the deployment. - /// + /// /// Notice that we do not use the received body /// and it is discarded with a wildcard _ receive(_: Vote) { @@ -92,10 +119,6 @@ contract Voter( require(!self.hasVoted, "This voter has participated already"); self.hasVoted = true; } - - get fun hasVoted(): Bool { - return self.hasVoted; - } } /// Defining a new message struct type, From 00c281a02ba9a4d3a99eae755904f65857f6cfd7 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 12 May 2025 04:11:27 +0200 Subject: [PATCH 09/10] more readme --- template/empty/README.md | 26 ++++++++++++++++++++++++++ template/quick-start/README.md | 2 ++ 2 files changed, 28 insertions(+) diff --git a/template/empty/README.md b/template/empty/README.md index 398fb2c..d62784c 100644 --- a/template/empty/README.md +++ b/template/empty/README.md @@ -30,3 +30,29 @@ Use `npx` to run any of the CLIs available. For example, to invoke the Tact form ```shell npx tact-fmt ``` + +## Learn more about Tact + +- [Website](https://tact-lang.org/) +- [Documentation](https://docs.tact-lang.org/) + - [Learn Tact in Y minutes](https://docs.tact-lang.org/book/learn-tact-in-y-minutes/) + - [Debugging and testing Tact contracts](https://docs.tact-lang.org/book/debug/) + - [Gas best practices](https://docs.tact-lang.org/book/gas-best-practices/) + - [Security best practices](https://docs.tact-lang.org/book/security-best-practices/) +- [Awesome Tact](https://github.com/tact-lang/awesome-tact) + +For more interesting contract examples, see the [Tact's DeFi Cookbook](https://github.com/tact-lang/defi-cookbook). + +## Community + +If you can’t find the answer in the [docs](https://docs.tact-lang.org), or you’ve tried to do some local testing and it still didn’t help — don’t hesitate to reach out to Tact’s flourishing community: + +- [`@tactlang` on Telegram](https://t.me/tactlang) - Main community chat and discussion group. +- [`@tactlang_ru` on Telegram](https://t.me/tactlang_ru) _(Russian)_ +- [`@tact_kitchen` on Telegram](https://t.me/tact_kitchen) - Channel with updates from the team. +- [`@tact_language` on X/Twitter](https://x.com/tact_language) +- [`tact-lang` organization on GitHub](https://github.com/tact-lang) +- [`@ton_studio` on Telegram](https://t.me/ton_studio) +- [`@thetonstudio` on X/Twitter](https://x.com/thetonstudio) + +Good luck on your coding adventure with ⚡ Tact! diff --git a/template/quick-start/README.md b/template/quick-start/README.md index a8048d2..3909eb8 100644 --- a/template/quick-start/README.md +++ b/template/quick-start/README.md @@ -78,6 +78,8 @@ npx tact-fmt - [Security best practices](https://docs.tact-lang.org/book/security-best-practices/) - [Awesome Tact](https://github.com/tact-lang/awesome-tact) +For more interesting contract examples, see the [Tact's DeFi Cookbook](https://github.com/tact-lang/defi-cookbook). + ## Community If you can’t find the answer in the [docs](https://docs.tact-lang.org), or you’ve tried to do some local testing and it still didn’t help — don’t hesitate to reach out to Tact’s flourishing community: From 0a3c6fce0e6938b85249252f5a9c7985a5dc3311 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Tue, 13 May 2025 11:13:00 +0200 Subject: [PATCH 10/10] chore: revert changes to empty template in order to apply them in another PR --- template/empty/README.md | 42 +-------------------------------- template/empty/gitignore | 4 +--- template/empty/src/main.spec.ts | 4 +++- template/empty/tact.config.json | 7 ------ 4 files changed, 5 insertions(+), 52 deletions(-) diff --git a/template/empty/README.md b/template/empty/README.md index d62784c..7674c56 100644 --- a/template/empty/README.md +++ b/template/empty/README.md @@ -9,50 +9,10 @@ ## How to use -- `pmRun build` – build contracts and the `.ts` wrappers +- `pmRun build` – build `.ts` API for contract - `pmRun test` – build contracts and run jest tests - `pmRun fmt` – fix source code formatting - `pmRun lint` – run semantic checks with `misti` linter - `pmRun verifier:testnet` – deploy contract to testnet - `pmRun verifier:mainnet` – deploy contract to mainnet - `pmRun fmt:check` – check source code formatting (for CI) - -## Available CLIs - -- `tact` – Tact compiler -- `tact-fmt` – Tact formatter -- `unboc` – Disassembler -- `@nowarp/misti` – Misti static analyzer -- `jest` – Jest testing framework - -Use `npx` to run any of the CLIs available. For example, to invoke the Tact formatter, execute this: - -```shell -npx tact-fmt -``` - -## Learn more about Tact - -- [Website](https://tact-lang.org/) -- [Documentation](https://docs.tact-lang.org/) - - [Learn Tact in Y minutes](https://docs.tact-lang.org/book/learn-tact-in-y-minutes/) - - [Debugging and testing Tact contracts](https://docs.tact-lang.org/book/debug/) - - [Gas best practices](https://docs.tact-lang.org/book/gas-best-practices/) - - [Security best practices](https://docs.tact-lang.org/book/security-best-practices/) -- [Awesome Tact](https://github.com/tact-lang/awesome-tact) - -For more interesting contract examples, see the [Tact's DeFi Cookbook](https://github.com/tact-lang/defi-cookbook). - -## Community - -If you can’t find the answer in the [docs](https://docs.tact-lang.org), or you’ve tried to do some local testing and it still didn’t help — don’t hesitate to reach out to Tact’s flourishing community: - -- [`@tactlang` on Telegram](https://t.me/tactlang) - Main community chat and discussion group. -- [`@tactlang_ru` on Telegram](https://t.me/tactlang_ru) _(Russian)_ -- [`@tact_kitchen` on Telegram](https://t.me/tact_kitchen) - Channel with updates from the team. -- [`@tact_language` on X/Twitter](https://x.com/tact_language) -- [`tact-lang` organization on GitHub](https://github.com/tact-lang) -- [`@ton_studio` on Telegram](https://t.me/ton_studio) -- [`@thetonstudio` on X/Twitter](https://x.com/thetonstudio) - -Good luck on your coding adventure with ⚡ Tact! diff --git a/template/empty/gitignore b/template/empty/gitignore index b456ead..2002ef4 100644 --- a/template/empty/gitignore +++ b/template/empty/gitignore @@ -4,6 +4,4 @@ output .idea/ .DS_Store/ *.swp -.helix/ -.vim/ -.nvim/ + diff --git a/template/empty/src/main.spec.ts b/template/empty/src/main.spec.ts index 8117c98..4da0882 100644 --- a/template/empty/src/main.spec.ts +++ b/template/empty/src/main.spec.ts @@ -5,10 +5,12 @@ import { ContractName } from "../output/projectName_ContractName"; it("should deploy correctly", async () => { const blockchain = await Blockchain.create(); + const contract = blockchain.openContract(await ContractName.fromInit()); + const deployer = await blockchain.treasury("deployer"); - // Send a message that `receive()` would handle + // call `receive()` const result = await contract.send(deployer.getSender(), { value: toNano(1) }, null); expect(result.transactions).toHaveTransaction({ diff --git a/template/empty/tact.config.json b/template/empty/tact.config.json index 6045288..ee63dc5 100644 --- a/template/empty/tact.config.json +++ b/template/empty/tact.config.json @@ -1,5 +1,4 @@ { - "$schema": "http://raw.githubusercontent.com/tact-lang/tact/main/src/config/configSchema.json", "projects": [ { "name": "projectName", @@ -13,12 +12,6 @@ "interfacesGetter": false, "experimental": { "inline": false - }, - "safety": { - "nullChecks": true - }, - "optimizations": { - "internalExternalReceiversOutsideMethodsMap": false } } }