diff --git a/dev-docs/CONTRIBUTING.md b/dev-docs/CONTRIBUTING.md index 397b3590cb..8b9f269c16 100644 --- a/dev-docs/CONTRIBUTING.md +++ b/dev-docs/CONTRIBUTING.md @@ -359,7 +359,7 @@ The code generator lives in the [`src/generator`](../src/generator) sub-folder w The implementation that we have right now is being refactored to produce FunC ASTs and then pretty-print those ASTs as strings instead of producing source FunC code in one step. Here is the relevant pull request: . One can find the end-to-end codegen test spec files in the [`src/test/e2e-emulated`](../src/test/e2e-emulated) folder. The test contracts are located in the subfolders of the [`src/test/e2e-emulated`](../src/test/e2e-emulated) folder. Many of those spec files test various language features in relative isolation. -An important spec file that tests argument passing semantics for functions and assignment semantics for variables is here: [`src/test/e2e-emulated/semantics.spec.ts`](../src/test/e2e-emulated/functions/semantics.spec.ts). +An important spec file that tests argument passing semantics for functions and assignment semantics for variables is here: [`src/test/e2e-emulated/functions/semantics.spec.ts`](../src/test/e2e-emulated/functions/semantics.spec.ts). Contracts with `inline` in the name of the file set `experimental.inline` config option to `true`. Contracts with `external` in the name of the file set the `external` config option to `true`. @@ -380,13 +380,14 @@ Benchmarks are located inside `src/benchmarks/`, one directory per benchmark: #### File & folder roles -| Path / file | Purpose | -| ------------------------ | -------------------------------------------------------- | -| `tact/` | Tact project that is being benchmarked | -| `func/` | Equivalent FunC project that we compare against | -| `.spec.ts` | Jest test spec that prepares and runs the benchmark | -| `results_gas.json` | Aggregated gas‑consumption results, updated by the CLI | -| `results_code_size.json` | Contract byte‑code size history, also updated by the CLI | +| Path / file | Purpose | +| --------------- | -------------------------------------------------------- | +| `tact/` | Tact project that is being benchmarked | +| `func/` | Equivalent FunC project that we compare against | +| `test.spec.ts` | Jest test spec for contract functionality testing | +| `bench.spec.ts` | Jest test spec for performance benchmarking | +| `gas.json` | Aggregated gas‑consumption results, updated by the CLI | +| `size.json` | Contract byte‑code size history, also updated by the CLI | > **CLI support** – All commands for creating, updating, or comparing benchmarks are documented in the [Updating Benchmarks](#benchmarks) section. diff --git a/package.json b/package.json index 7909924aba..9faa0cdf1f 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,9 @@ "test": "jest", "test:fast": "jest --config=./jest-fast.config.js", "test:allure": "rimraf ./allure-results && yarn test && allure serve allure-results", - "bench": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=true jest ./src/benchmarks", - "bench:ci": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=false jest ./src/benchmarks", + "bench": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=true jest --testMatch=\"**/src/benchmarks/**/bench.spec.ts\"", + "bench:test": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=false jest --testMatch=\"**/src/benchmarks/**/test.spec.ts\"", + "bench:ci": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=true jest --testMatch=\"**/src/benchmarks/**/bench.spec.ts\" && cross-env PRINT_TABLE=false jest --testMatch=\"**/src/benchmarks/**/test.spec.ts\"", "bench:update": "yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=true ts-node src/benchmarks/update.build.ts", "bench:add": "ts-node src/benchmarks/prompt.build.ts && yarn gen:contracts:benchmarks && cross-env PRINT_TABLE=true ADD=true ts-node src/benchmarks/update.build.ts", "coverage": "cross-env COVERAGE=true NODE_OPTIONS=--max_old_space_size=5120 jest --config=./jest-ci.config.js", diff --git a/spell/cspell-list.txt b/spell/cspell-list.txt index 11b6b8b52d..e4bf8af745 100644 --- a/spell/cspell-list.txt +++ b/spell/cspell-list.txt @@ -1,3 +1,4 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU Aksakov Aliaksandr alnum diff --git a/src/benchmarks/escrow/escrow.spec.ts b/src/benchmarks/escrow/bench.spec.ts similarity index 98% rename from src/benchmarks/escrow/escrow.spec.ts rename to src/benchmarks/escrow/bench.spec.ts index 62ed9e6a6e..69f977a72e 100644 --- a/src/benchmarks/escrow/escrow.spec.ts +++ b/src/benchmarks/escrow/bench.spec.ts @@ -24,8 +24,8 @@ import type { Cancel, } from "@/benchmarks/escrow/tact/output/escrow_Escrow"; -import benchmarkResults from "@/benchmarks/escrow/results_gas.json"; -import benchmarkCodeSizeResults from "@/benchmarks/escrow/results_code_size.json"; +import benchmarkResults from "@/benchmarks/escrow/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/escrow/size.json"; const loadFunCEscrowBoc = () => { const bocEscrow = readFileSync( diff --git a/src/benchmarks/escrow/results_gas.json b/src/benchmarks/escrow/gas.json similarity index 100% rename from src/benchmarks/escrow/results_gas.json rename to src/benchmarks/escrow/gas.json diff --git a/src/benchmarks/escrow/results_code_size.json b/src/benchmarks/escrow/size.json similarity index 100% rename from src/benchmarks/escrow/results_code_size.json rename to src/benchmarks/escrow/size.json diff --git a/src/benchmarks/jetton/jetton.spec.ts b/src/benchmarks/jetton/bench.spec.ts similarity index 77% rename from src/benchmarks/jetton/jetton.spec.ts rename to src/benchmarks/jetton/bench.spec.ts index 86ad62938a..6a8f78bb65 100644 --- a/src/benchmarks/jetton/jetton.spec.ts +++ b/src/benchmarks/jetton/bench.spec.ts @@ -1,65 +1,39 @@ import "@ton/test-utils"; -import { - Address, - Cell, - beginCell, - toNano, - contractAddress, - type Sender, -} from "@ton/core"; +import { Address, Cell, beginCell, toNano, type Sender } from "@ton/core"; import { Blockchain } from "@ton/sandbox"; import type { SandboxContract, TreasuryContract } from "@ton/sandbox"; import { - generateResults, getStateSizeForAccount, - generateCodeSizeResults, getUsedGas, - printBenchmarkTable, type BenchmarkResult, type CodeSizeResult, } from "@/benchmarks/utils/gas"; -import { resolve } from "path"; -import { readFileSync } from "fs"; -import { posixNormalize } from "@/utils/filePath"; -import { + +import type { JettonMinter, - type JettonUpdateContent, - type Mint, - type ProvideWalletAddress, + JettonUpdateContent, + Mint, + ProvideWalletAddress, } from "@/benchmarks/jetton/tact/output/minter_JettonMinter"; + import { JettonWallet, type JettonTransfer, type JettonBurn, } from "@/benchmarks/jetton/tact/output/minter_JettonWallet"; -import benchmarkResults from "@/benchmarks/jetton/results_gas.json"; -import benchmarkCodeSizeResults from "@/benchmarks/jetton/results_code_size.json"; import { step, parameter } from "@/test/allure/allure"; +import type { + FromInitMinter, + FromInitWallet, +} from "@/benchmarks/jetton/tests/utils"; -const loadFunCJettonsBoc = () => { - const bocMinter = readFileSync( - posixNormalize( - resolve(__dirname, "./func/output/jetton-minter-discoverable.boc"), - ), - ); - - const bocWallet = readFileSync( - posixNormalize(resolve(__dirname, "./func/output/jetton-wallet.boc")), - ); - - return { bocMinter, bocWallet }; -}; - -function testJetton( +function benchJetton( benchmarkResults: BenchmarkResult, codeSizeResults: CodeSizeResult, - fromInit: ( - totalSupply: bigint, - owner: Address, - jettonContent: Cell, - ) => Promise, + fromInit: FromInitMinter, + _fromInitWallet: FromInitWallet, ) { let blockchain: Blockchain; let deployer: SandboxContract; @@ -339,50 +313,6 @@ function testJetton( }); } -describe("Jetton Gas Tests", () => { - const fullResults = generateResults(benchmarkResults); - const fullCodeSizeResults = generateCodeSizeResults( - benchmarkCodeSizeResults, - ); - - describe("func", () => { - const funcCodeSize = fullCodeSizeResults.at(0)!; - const funcResult = fullResults.at(0)!; - - function fromInit(salt: bigint, admin: Address, _content: Cell) { - const jettonData = loadFunCJettonsBoc(); - const minterCell = Cell.fromBoc(jettonData.bocMinter)[0]!; - const walletCell = Cell.fromBoc(jettonData.bocWallet)[0]!; - - const stateInitMinter = beginCell() - .storeCoins(0) - .storeAddress(admin) - .storeRef(beginCell().storeUint(1, 1).endCell()) // as salt - .storeRef(walletCell) - .endCell(); - - const init = { code: minterCell, data: stateInitMinter }; - const address = contractAddress(0, init); - return Promise.resolve(new JettonMinter(address, init)); - } - - testJetton(funcResult, funcCodeSize, fromInit); - }); - - describe("tact", () => { - const tactCodeSize = fullCodeSizeResults.at(-1)!; - const tactResult = fullResults.at(-1)!; - testJetton( - tactResult, - tactCodeSize, - JettonMinter.fromInit.bind(JettonMinter), - ); - }); +import { run } from "@/benchmarks/jetton/run"; - afterAll(() => { - printBenchmarkTable(fullResults, fullCodeSizeResults, { - implementationName: "FunC", - printMode: "full", - }); - }); -}); +run(benchJetton); diff --git a/src/benchmarks/jetton/results_gas.json b/src/benchmarks/jetton/gas.json similarity index 100% rename from src/benchmarks/jetton/results_gas.json rename to src/benchmarks/jetton/gas.json diff --git a/src/benchmarks/jetton/run.ts b/src/benchmarks/jetton/run.ts new file mode 100644 index 0000000000..97ac45769d --- /dev/null +++ b/src/benchmarks/jetton/run.ts @@ -0,0 +1,120 @@ +import "@ton/test-utils"; +import { type Address, Cell, beginCell, contractAddress } from "@ton/core"; + +import { + generateResults, + generateCodeSizeResults, + printBenchmarkTable, + type BenchmarkResult, + type CodeSizeResult, +} from "@/benchmarks/utils/gas"; +import { resolve } from "path"; +import { readFileSync } from "fs"; +import { posixNormalize } from "@/utils/filePath"; +import { JettonMinter } from "@/benchmarks/jetton/tact/output/minter_JettonMinter"; +import { JettonWallet } from "@/benchmarks/jetton/tact/output/minter_JettonWallet"; + +import benchmarkResults from "@/benchmarks/jetton/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/jetton/size.json"; +import type { + FromInitMinter, + FromInitWallet, +} from "@/benchmarks/jetton/tests/utils"; + +const loadFunCJettonsBoc = () => { + const bocMinter = readFileSync( + posixNormalize( + resolve(__dirname, "./func/output/jetton-minter-discoverable.boc"), + ), + ); + + const bocWallet = readFileSync( + posixNormalize(resolve(__dirname, "./func/output/jetton-wallet.boc")), + ); + + return { bocMinter, bocWallet }; +}; +export const run = ( + testJetton: ( + benchmarkResults: BenchmarkResult, + codeSizeResults: CodeSizeResult, + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, + ) => void, +) => { + describe("Jetton Gas Tests", () => { + const fullResults = generateResults(benchmarkResults); + const fullCodeSizeResults = generateCodeSizeResults( + benchmarkCodeSizeResults, + ); + + describe("func", () => { + const funcCodeSize = fullCodeSizeResults.at(0)!; + const funcResult = fullResults.at(0)!; + + const fromInitMinter = ( + salt: bigint, + admin: Address, + _content: Cell, + ) => { + const jettonData = loadFunCJettonsBoc(); + const minterCell = Cell.fromBoc(jettonData.bocMinter)[0]!; + const walletCell = Cell.fromBoc(jettonData.bocWallet)[0]!; + + const stateInitMinter = beginCell() + .storeCoins(0) + .storeAddress(admin) + .storeRef(beginCell().storeUint(1, 1).endCell()) // as salt + .storeRef(walletCell) + .endCell(); + + const init = { code: minterCell, data: stateInitMinter }; + const address = contractAddress(0, init); + return Promise.resolve(new JettonMinter(address, init)); + }; + + const fromInitWallet = ( + owner: Address, + jettonMinter: Address, + jettonAmount: bigint, + ) => { + const __code = Cell.fromBoc(loadFunCJettonsBoc().bocWallet)[0]!; + const __data = beginCell() + .storeCoins(jettonAmount) + .storeAddress(owner) + .storeAddress(jettonMinter) + .storeRef(__code) + .endCell(); + + const __gen_init = { code: __code, data: __data }; + const address = contractAddress(0, __gen_init); + return Promise.resolve(new JettonWallet(address, __gen_init)); + }; + + testJetton( + funcResult, + funcCodeSize, + fromInitMinter, + fromInitWallet, + ); + }); + + describe("tact", () => { + const tactCodeSize = fullCodeSizeResults.at(-1)!; + const tactResult = fullResults.at(-1)!; + testJetton( + tactResult, + tactCodeSize, + JettonMinter.fromInit.bind(JettonMinter), + JettonWallet.fromInit.bind(JettonWallet), + ); + }); + + afterAll(() => { + printBenchmarkTable(fullResults, fullCodeSizeResults, { + implementationName: "FunC", + printMode: "full", + }); + }); + }); +}; diff --git a/src/benchmarks/jetton/results_code_size.json b/src/benchmarks/jetton/size.json similarity index 100% rename from src/benchmarks/jetton/results_code_size.json rename to src/benchmarks/jetton/size.json diff --git a/src/benchmarks/jetton/tact/wallet.tact b/src/benchmarks/jetton/tact/wallet.tact index 21dd906e8d..c93f811fb6 100644 --- a/src/benchmarks/jetton/tact/wallet.tact +++ b/src/benchmarks/jetton/tact/wallet.tact @@ -1,5 +1,7 @@ import "./messages"; +asm fun forceBasechainFunc(address: Address) { REWRITESTDADDR DROP 333 THROWIF } + contract JettonWallet( owner: Address, master: Address, @@ -9,7 +11,7 @@ contract JettonWallet( const gasConsumption: Int = ton("0.015"); receive(msg: JettonTransfer) { - forceBasechain(msg.destination); + forceBasechainFunc(msg.destination); throwUnless(705, sender() == self.owner); self.balance -= msg.amount; diff --git a/src/benchmarks/jetton/test.spec.ts b/src/benchmarks/jetton/test.spec.ts new file mode 100644 index 0000000000..3ad411f8bd --- /dev/null +++ b/src/benchmarks/jetton/test.spec.ts @@ -0,0 +1,23 @@ +import type { BenchmarkResult, CodeSizeResult } from "@/benchmarks/utils/gas"; +import { run } from "@/benchmarks/jetton/run"; +import { testMinter } from "@/benchmarks/jetton/tests/minter"; +import { testWallet, testBounces } from "@/benchmarks/jetton/tests/wallet"; +import type { + FromInitMinter, + FromInitWallet, +} from "@/benchmarks/jetton/tests/utils"; + +const testJetton = ( + _benchmarkResults: BenchmarkResult, + _codeSizeResults: CodeSizeResult, + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, +) => { + testMinter(fromInitMinter, fromInitWallet); + testWallet(fromInitMinter, fromInitWallet); + testBounces(fromInitMinter, fromInitWallet); +}; + +describe("jetton", () => { + run(testJetton); +}); diff --git a/src/benchmarks/jetton/tests/minter.ts b/src/benchmarks/jetton/tests/minter.ts new file mode 100644 index 0000000000..0c9771c9e4 --- /dev/null +++ b/src/benchmarks/jetton/tests/minter.ts @@ -0,0 +1,224 @@ +import { beginCell, toNano } from "@ton/core"; + +import "@ton/test-utils"; +import { + sendMint, + getTotalSupply, + getJettonBalance, + getAdminAddress, + errors, + sendChangeAdmin, + getContent, + jettonContentToCell, + sendChangeContent, + type FromInitMinter, + type FromInitWallet, + globalSetup, +} from "@/benchmarks/jetton/tests/utils"; + +export const testMinter = ( + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, +) => { + const setup = async () => { + return await globalSetup(fromInitMinter, fromInitWallet); + }; + + describe("JettonMinter", () => { + // implementation detail + it("minter admin should be able to mint jettons", async () => { + const { jettonMinter, deployer, userWallet, notDeployer } = + await setup(); + // can mint from deployer + let initialTotalSupply = await getTotalSupply(jettonMinter); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = toNano("1000.23"); + const mintResult = await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + initialJettonBalance, + toNano("0.05"), + toNano("1"), + ); + + // Here was the check, that excesses are send to JettonMinter. + // This is an implementation-defined behavior + // In my implementation, excesses are sent to the deployer + expect(mintResult.transactions).toHaveTransaction({ + // excesses + from: deployerJettonWallet.address, + to: deployer.address, + }); + + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply + initialJettonBalance, + ); + initialTotalSupply += initialJettonBalance; + // can mint from deployer again + const additionalJettonBalance = toNano("2.31"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + additionalJettonBalance, + toNano("0.05"), + toNano("1"), + ); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance + additionalJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply + additionalJettonBalance, + ); + initialTotalSupply += additionalJettonBalance; + // can mint to other address + const otherJettonBalance = toNano("3.12"); + await sendMint( + jettonMinter, + deployer.getSender(), + notDeployer.address, + otherJettonBalance, + toNano("0.05"), + toNano("1"), + ); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + otherJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply + otherJettonBalance, + ); + }); + + // implementation detail + it("not a minter admin should not be able to mint jettons", async () => { + const { jettonMinter, deployer, userWallet, notDeployer } = + await setup(); + const initialTotalSupply = await getTotalSupply(jettonMinter); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const unAuthMintResult = await sendMint( + jettonMinter, + notDeployer.getSender(), + deployer.address, + toNano("777"), + toNano("0.05"), + toNano("1"), + ); + + expect(unAuthMintResult.transactions).toHaveTransaction({ + from: notDeployer.address, + to: jettonMinter.address, + aborted: true, + exitCode: errors["Incorrect sender"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply, + ); + }); + + // Implementation detail + it("minter admin can change admin", async () => { + const { jettonMinter, deployer, notDeployer } = await setup(); + const adminBefore = await getAdminAddress(jettonMinter); + expect(adminBefore).toEqualAddress(deployer.address); + const res = await sendChangeAdmin( + jettonMinter, + deployer.getSender(), + notDeployer.address, + ); + expect(res.transactions).toHaveTransaction({ + from: deployer.address, + on: jettonMinter.address, + success: true, + }); + + const adminAfter = await getAdminAddress(jettonMinter); + expect(adminAfter).toEqualAddress(notDeployer.address); + await sendChangeAdmin( + jettonMinter, + notDeployer.getSender(), + deployer.address, + ); + expect( + (await getAdminAddress(jettonMinter)).equals(deployer.address), + ).toBe(true); + }); + + it("not a minter admin can not change admin", async () => { + const { jettonMinter, deployer, notDeployer } = await setup(); + const adminBefore = await getAdminAddress(jettonMinter); + expect(adminBefore).toEqualAddress(deployer.address); + const changeAdmin = await sendChangeAdmin( + jettonMinter, + notDeployer.getSender(), + notDeployer.address, + ); + expect( + (await getAdminAddress(jettonMinter)).equals(deployer.address), + ).toBe(true); + expect(changeAdmin.transactions).toHaveTransaction({ + from: notDeployer.address, + on: jettonMinter.address, + aborted: true, + exitCode: errors["Incorrect sender"], + }); + }); + + it("minter admin can change content", async () => { + const { jettonMinter, deployer, defaultContent } = await setup(); + const newContent = jettonContentToCell({ + type: 1, + uri: "https://totally_new_jetton.org/content.json", + }); + expect( + (await getContent(jettonMinter)).equals(defaultContent), + ).toBe(true); + await sendChangeContent( + jettonMinter, + deployer.getSender(), + newContent, + ); + expect((await getContent(jettonMinter)).equals(newContent)).toBe( + true, + ); + await sendChangeContent( + jettonMinter, + deployer.getSender(), + defaultContent, + ); + expect( + (await getContent(jettonMinter)).equals(defaultContent), + ).toBe(true); + }); + + it("not a minter admin can not change content", async () => { + const { jettonMinter, notDeployer, defaultContent } = await setup(); + const newContent = beginCell().storeUint(1, 1).endCell(); + const changeContent = await sendChangeContent( + jettonMinter, + notDeployer.getSender(), + newContent, + ); + expect( + (await getContent(jettonMinter)).equals(defaultContent), + ).toBe(true); + expect(changeContent.transactions).toHaveTransaction({ + from: notDeployer.address, + to: jettonMinter.address, + aborted: true, + exitCode: errors["Incorrect sender"], + }); + }); + }); +}; diff --git a/src/benchmarks/jetton/tests/utils.ts b/src/benchmarks/jetton/tests/utils.ts new file mode 100644 index 0000000000..586331184b --- /dev/null +++ b/src/benchmarks/jetton/tests/utils.ts @@ -0,0 +1,317 @@ +import { toNano, beginCell, Address, Builder } from "@ton/core"; + +import type { Cell, Sender } from "@ton/core"; +import type { + JettonMinter, + Mint, + ChangeOwner, + JettonUpdateContent, + ProvideWalletAddress, + JettonBurn, + JettonTransfer, +} from "@/benchmarks/jetton/tact/output/minter_JettonMinter"; +import { + Blockchain, + type SandboxContract, + type SendMessageResult, +} from "@ton/sandbox"; + +import { randomBytes } from "crypto"; +import { JettonWallet } from "@/benchmarks/jetton/tact/output/minter_JettonWallet"; +import { setStoragePrices } from "@/test/utils/gasUtils"; + +export const randomAddress = (wc: number = 0) => { + const buf = Buffer.alloc(32); + for (let i = 0; i < buf.length; i++) { + buf[i] = Math.floor(Math.random() * 256); + } + return new Address(wc, buf); +}; + +const getRandom = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +export const getRandomInt = (min: number, max: number) => { + return Math.round(getRandom(min, max)); +}; + +export const storeBigPayload = (curBuilder: Builder, maxDepth: number = 5) => { + const rootBuilder = curBuilder; + + function dfs(builder: Builder, currentDepth: number) { + if (currentDepth >= maxDepth) { + return; + } + // Cell has a capacity of 1023 bits, so we can store 127 bytes max + builder.storeBuffer(randomBytes(127)); + // Store all 4 references + for (let i = 0; i < 4; i++) { + const newBuilder = beginCell(); + dfs(newBuilder, currentDepth + 1); + builder.storeRef(newBuilder.endCell()); + } + } + + dfs(rootBuilder, 0); // Start DFS with depth 0 + return rootBuilder; +}; + +export const sendMint = async ( + jettonMinter: SandboxContract, + via: Sender, + to: Address, + jettonAmount: bigint, + forwardTonAmount: bigint, + totalTonAmount: bigint, +): Promise => { + if (totalTonAmount <= forwardTonAmount) { + throw new Error( + "Total TON amount should be greater than the forward amount", + ); + } + const msg: Mint = { + $$type: "Mint", + queryId: 0n, + receiver: to, + tonAmount: totalTonAmount, + mintMessage: { + $$type: "JettonTransferInternal", + queryId: 0n, + amount: jettonAmount, + sender: via.address!, + responseDestination: via.address!, + forwardTonAmount: forwardTonAmount, + forwardPayload: beginCell().storeUint(0, 1).asSlice(), + }, + }; + return await jettonMinter.send( + via, + { value: totalTonAmount + toNano("0.015") }, + msg, + ); +}; + +export const sendChangeAdmin = async ( + jettonMinter: SandboxContract, + via: Sender, + newOwner: Address, +): Promise => { + const msg: ChangeOwner = { + $$type: "ChangeOwner", + queryId: 0n, + newOwner: newOwner, + }; + return await jettonMinter.send(via, { value: toNano("0.05") }, msg); +}; + +export const sendChangeContent = async ( + jettonMinter: SandboxContract, + via: Sender, + content: Cell, +): Promise => { + const msg: JettonUpdateContent = { + $$type: "JettonUpdateContent", + queryId: 0n, + content: content, + }; + return await jettonMinter.send(via, { value: toNano("0.05") }, msg); +}; + +export const sendDiscovery = async ( + jettonMinter: SandboxContract, + via: Sender, + address: Address, + includeAddress: boolean, + value: bigint = toNano("0.1"), +): Promise => { + const msg: ProvideWalletAddress = { + $$type: "ProvideWalletAddress", + queryId: 0n, + ownerAddress: address, + includeAddress: includeAddress, + }; + return await jettonMinter.send(via, { value: value }, msg); +}; + +export const getTotalSupply = async ( + jettonMinter: SandboxContract, +) => { + const res = await jettonMinter.getGetJettonData(); + return res.totalSupply; +}; + +export const getAdminAddress = async ( + jettonMinter: SandboxContract, +) => { + const res = await jettonMinter.getGetJettonData(); + return res.adminAddress; +}; + +export const getContent = async ( + jettonMinter: SandboxContract, +) => { + const res = await jettonMinter.getGetJettonData(); + return res.jettonContent; +}; + +export const getJettonBalance = async ( + jettonWallet: SandboxContract, +) => { + return (await jettonWallet.getGetWalletData()).balance; +}; + +export const errors = { + "Incorrect sender": 73, + "Unauthorized burn": 74, + "Not a basechain address": 333, + "Incorrect sender jetton": 705, + "Incorrect balance after send": 706, + "Incorrect sender wallet": 707, + "Insufficient amount of TON attached": 709, +}; + +export const jettonContentToCell = (content: { type: 0 | 1; uri: string }) => { + return beginCell() + .storeUint(content.type, 8) + .storeStringTail(content.uri) // Snake logic under the hood + .endCell(); +}; + +export const sendTransfer = async ( + jettonWallet: SandboxContract, + via: Sender, + value: bigint, + jettonAmount: bigint, + to: Address, + responseAddress: Address, + customPayload: Cell | null, + forwardTonAmount: bigint, + forwardPayload: Cell | null, +): Promise => { + const parsedForwardPayload = + forwardPayload != null + ? forwardPayload.beginParse() + : new Builder().storeUint(0, 1).endCell().beginParse(); + + const msg: JettonTransfer = { + $$type: "JettonTransfer", + queryId: 0n, + amount: jettonAmount, + destination: to, + responseDestination: responseAddress, + customPayload: customPayload, + forwardTonAmount: forwardTonAmount, + forwardPayload: parsedForwardPayload, + }; + + return await jettonWallet.send(via, { value }, msg); +}; + +export const sendBurn = async ( + jettonWallet: SandboxContract, + via: Sender, + value: bigint, + jettonAmount: bigint, + responseAddress: Address, + customPayload: Cell | null, +): Promise => { + const msg: JettonBurn = { + $$type: "JettonBurn", + queryId: 0n, + amount: jettonAmount, + responseDestination: responseAddress, + customPayload: customPayload, + }; + + return await jettonWallet.send(via, { value }, msg); +}; + +export type FromInitMinter = ( + totalSupply: bigint, + owner: Address, + jettonContent: Cell, +) => Promise; +export type FromInitWallet = ( + owner: Address, + jettonMinter: Address, + jettonAmount: bigint, +) => Promise; + +export const globalSetup = async ( + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, +) => { + const blockchain = await Blockchain.create(); + const config = blockchain.config; + blockchain.setConfig( + setStoragePrices(config, { + unixTimeSince: 0, + bitPricePerSecond: 0n, + cellPricePerSecond: 0n, + masterChainBitPricePerSecond: 0n, + masterChainCellPricePerSecond: 0n, + }), + ); + + const deployer = await blockchain.treasury("deployer"); + const notDeployer = await blockchain.treasury("notDeployer"); + + const defaultContent = beginCell() + .storeStringTail("defaultContent") + .endCell(); + + const msg: JettonUpdateContent = { + $$type: "JettonUpdateContent", + queryId: 0n, + content: defaultContent, + }; + + const jettonMinter = blockchain.openContract( + await fromInitMinter(0n, deployer.address, defaultContent), + ); + + const deployResult = await jettonMinter.send( + deployer.getSender(), + { value: toNano("0.1") }, + msg, + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonMinter.address, + deploy: true, + success: true, + }); + + const jettonWallet = blockchain.openContract( + await fromInitWallet(deployer.address, jettonMinter.address, 0n), + ); + + const userWallet = async (address: Address) => { + const wallet = blockchain.openContract( + new JettonWallet(await jettonMinter.getGetWalletAddress(address)), + ); + + await sendMint( + jettonMinter, + deployer.getSender(), + address, + 0n, + 0n, + toNano(1), + ); + + return wallet; + }; + + return { + blockchain, + deployer, + notDeployer, + jettonMinter, + jettonWallet, + userWallet, + defaultContent, + }; +}; diff --git a/src/benchmarks/jetton/tests/wallet.ts b/src/benchmarks/jetton/tests/wallet.ts new file mode 100644 index 0000000000..68248c58d7 --- /dev/null +++ b/src/benchmarks/jetton/tests/wallet.ts @@ -0,0 +1,1069 @@ +import { Address, beginCell, toNano } from "@ton/core"; +import { internal } from "@ton/sandbox"; + +import { + JettonMinter, + storeJettonBurn, + storeJettonTransfer, +} from "@/benchmarks/jetton/tact/output/minter_JettonMinter"; + +import "@ton/test-utils"; +import { + sendMint, + getTotalSupply, + getJettonBalance, + errors, + sendTransfer, + sendBurn, + getRandomInt, + randomAddress, + sendDiscovery, + storeBigPayload, + type FromInitMinter, + type FromInitWallet, + globalSetup, +} from "@/benchmarks/jetton/tests/utils"; + +export const testWallet = ( + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, +) => { + const setup = async () => { + return await globalSetup(fromInitMinter, fromInitWallet); + }; + + describe("JettonWallet", () => { + it("wallet owner should be able to send jettons", async () => { + const { deployer, jettonMinter, notDeployer, userWallet } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + + const sentAmount = toNano("0.5"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + sentAmount, + 0n, + toNano(1), + ); + + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const initialTotalSupply = await getTotalSupply(jettonMinter); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + + const forwardAmount = toNano("0.05"); + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + null, + ); + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: notDeployerJettonWallet.address, + to: deployer.address, + }); + expect(sendResult.transactions).toHaveTransaction({ + // notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + value: forwardAmount, + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance - sentAmount, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2 + sentAmount, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply, + ); + }); + + it("not wallet owner should not be able to send jettons", async () => { + const { deployer, jettonMinter, notDeployer, userWallet } = + await setup(); + + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const initialTotalSupply = await getTotalSupply(jettonMinter); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + const sentAmount = toNano("0.5"); + const sendResult = await sendTransfer( + deployerJettonWallet, + notDeployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + toNano("0.05"), + null, + ); + expect(sendResult.transactions).toHaveTransaction({ + from: notDeployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Incorrect sender jetton"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply, + ); + }); + + it("impossible to send too much jettons", async () => { + const { deployer, notDeployer, userWallet } = await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + const sentAmount = initialJettonBalance + 1n; + const forwardAmount = toNano("0.05"); + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + null, + ); + expect(sendResult.transactions).toHaveTransaction({ + from: deployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Incorrect balance after send"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2, + ); + }); + + it("correctly sends forward_payload in place", async () => { + const { jettonMinter, deployer, notDeployer, userWallet } = + await setup(); + + const sentAmount = toNano("0.5"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + sentAmount, + 0n, + toNano(1), + ); + + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + + const forwardAmount = toNano("0.05"); + const forwardPayload = beginCell() + .storeUint(0x123456789n, 128) + .endCell(); + // This block checks forward_payload in place (Either bit equals 0) + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + forwardPayload, + ); + + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: notDeployerJettonWallet.address, + to: deployer.address, + }); + /* + transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + */ + expect(sendResult.transactions).toHaveTransaction({ + // notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + value: forwardAmount, + body: beginCell() + .storeUint(JettonMinter.opcodes.JettonNotification, 32) + .storeUint(0, 64) // default queryId + .storeCoins(sentAmount) + .storeAddress(deployer.address) + .storeSlice(forwardPayload.beginParse()) // Doing this because forward_payload is already Cell with 1 bit 1 and one ref. + .endCell(), + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance - sentAmount, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2 + sentAmount, + ); + }); + + // There was no such test in official implementation + it("correctly sends forward_payload in ref", async () => { + const { jettonMinter, deployer, notDeployer, userWallet } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + + const sentAmount = toNano("0.5"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + sentAmount, + 0n, + toNano(1), + ); + + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + + const forwardAmount = toNano("0.05"); + // This block checks forward_payload in separate ref (Either bit equals 1) + const forwardPayload = beginCell() + .storeUint(1, 1) + .storeRef(beginCell().storeUint(0x123456789n, 128).endCell()) + .endCell(); + + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + forwardPayload, + ); + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: notDeployerJettonWallet.address, + to: deployer.address, + }); + /* + transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + */ + expect(sendResult.transactions).toHaveTransaction({ + // notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + value: forwardAmount, + body: beginCell() + .storeUint(JettonMinter.opcodes.JettonNotification, 32) + .storeUint(0, 64) // default queryId + .storeCoins(sentAmount) + .storeAddress(deployer.address) + .storeSlice(forwardPayload.beginParse()) // Doing this because forward_payload is already Cell with 1 bit 1 and one ref. + .endCell(), + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance - sentAmount, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2 + sentAmount, + ); + }); + + it("no forward_ton_amount - no forward", async () => { + const { deployer, jettonMinter, notDeployer, userWallet } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + + const sentAmount = toNano("0.5"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + sentAmount, + 0n, + toNano(1), + ); + + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const initialJettonBalance2 = await getJettonBalance( + notDeployerJettonWallet, + ); + + const forwardAmount = 0n; + const forwardPayload = beginCell() + .storeUint(0x123456789n, 128) + .endCell(); + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // tons + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + forwardPayload, + ); + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: notDeployerJettonWallet.address, + to: deployer.address, + }); + + expect(sendResult.transactions).not.toHaveTransaction({ + // no notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance - sentAmount, + ); + expect(await getJettonBalance(notDeployerJettonWallet)).toEqual( + initialJettonBalance2 + sentAmount, + ); + }); + + it("check revert on not enough tons for forward", async () => { + const { deployer, jettonMinter, notDeployer, userWallet } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + + const sentAmount = toNano("0.1"); + + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + sentAmount, + 0n, + toNano(1), + ); + + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + await deployer.send({ + value: toNano("1"), + bounce: false, + to: deployerJettonWallet.address, + }); + + const forwardAmount = toNano("0.3"); + const forwardPayload = beginCell() + .storeUint(0x123456789n, 128) + .endCell(); + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + forwardAmount, // not enough tons, no tons for gas + sentAmount, + notDeployer.address, + deployer.address, + null, + forwardAmount, + forwardPayload, + ); + expect(sendResult.transactions).toHaveTransaction({ + from: deployer.address, + on: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Insufficient amount of TON attached"], + }); + // Make sure value bounced + expect(sendResult.transactions).toHaveTransaction({ + from: deployerJettonWallet.address, + on: deployer.address, + inMessageBounced: true, + success: true, + }); + + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + }); + + // implementation detail + it("wallet does not accept internal_transfer not from wallet", async () => { + const { deployer, notDeployer, userWallet, blockchain } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + /* + internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress + response_address:MsgAddress + forward_ton_amount:(VarUInteger 16) + forward_payload:(Either Cell ^Cell) + = InternalMsgBody; + */ + const internalTransfer = beginCell() + .storeUint(0x178d4519, 32) + .storeUint(0, 64) // default queryId + .storeCoins(toNano("0.01")) + .storeAddress(deployer.address) + .storeAddress(deployer.address) + .storeCoins(toNano("0.05")) + .storeUint(0, 1) + .endCell(); + const sendResult = await blockchain.sendMessage( + internal({ + from: notDeployer.address, + to: deployerJettonWallet.address, + body: internalTransfer, + value: toNano("0.3"), + }), + ); + expect(sendResult.transactions).toHaveTransaction({ + from: notDeployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Incorrect sender wallet"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + }); + + it("wallet owner should be able to burn jettons", async () => { + const { deployer, jettonMinter, userWallet } = await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const burnAmount = toNano("0.01"); + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + burnAmount, + 0n, + toNano(1), + ); + + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const initialTotalSupply = await getTotalSupply(jettonMinter); + + const sendResult = await sendBurn( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // ton amount + burnAmount, + deployer.address, + null, + ); // amount, response address, custom payload + expect(sendResult.transactions).toHaveTransaction({ + // burn notification + from: deployerJettonWallet.address, + to: jettonMinter.address, + }); + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: jettonMinter.address, + to: deployer.address, + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance - burnAmount, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply - burnAmount, + ); + }); + + it("not wallet owner should not be able to burn jettons", async () => { + const { deployer, jettonMinter, userWallet, notDeployer } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const initialTotalSupply = await getTotalSupply(jettonMinter); + const burnAmount = toNano("0.01"); + const sendResult = await sendBurn( + deployerJettonWallet, + notDeployer.getSender(), + toNano("0.1"), // ton amount + burnAmount, + deployer.address, + null, + ); // amount, response address, custom payload + expect(sendResult.transactions).toHaveTransaction({ + from: notDeployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Incorrect sender jetton"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply, + ); + }); + + it("wallet owner can not burn more jettons than it has", async () => { + const { deployer, jettonMinter, userWallet } = await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const initialJettonBalance = + await getJettonBalance(deployerJettonWallet); + const initialTotalSupply = await getTotalSupply(jettonMinter); + const burnAmount = initialJettonBalance + 1n; + const sendResult = await sendBurn( + deployerJettonWallet, + deployer.getSender(), + toNano("0.1"), // ton amount + burnAmount, + deployer.address, + null, + ); // amount, response address, custom payload + expect(sendResult.transactions).toHaveTransaction({ + from: deployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Incorrect balance after send"], + }); + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + initialJettonBalance, + ); + expect(await getTotalSupply(jettonMinter)).toEqual( + initialTotalSupply, + ); + }); + + it("minter should only accept burn messages from jetton wallets", async () => { + const { userWallet, deployer, jettonMinter, blockchain } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const burnAmount = toNano("1"); + + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + burnAmount, + 0n, + toNano(1), + ); + + const burnNotification = (amount: bigint, addr: Address) => { + return beginCell() + .storeUint(JettonMinter.opcodes.JettonBurnNotification, 32) + .storeUint(0, 64) + .storeCoins(amount) + .storeAddress(addr) + .storeAddress(deployer.address) + .endCell(); + }; + + let res = await blockchain.sendMessage( + internal({ + from: deployerJettonWallet.address, + to: jettonMinter.address, + body: burnNotification(burnAmount, randomAddress(0)), + value: toNano("0.1"), + }), + ); + + expect(res.transactions).toHaveTransaction({ + from: deployerJettonWallet.address, + to: jettonMinter.address, + aborted: true, + exitCode: errors["Unauthorized burn"], + }); + + res = await blockchain.sendMessage( + internal({ + from: deployerJettonWallet.address, + to: jettonMinter.address, + body: burnNotification(burnAmount, deployer.address), + value: toNano("0.1"), + }), + ); + + expect(res.transactions).toHaveTransaction({ + from: deployerJettonWallet.address, + to: jettonMinter.address, + success: true, + }); + }); + + // TEP-89 + it("report correct discovery address", async () => { + const { userWallet, deployer, jettonMinter, notDeployer } = + await setup(); + let discoveryResult = await sendDiscovery( + jettonMinter, + deployer.getSender(), + deployer.address, + true, + ); + /* + take_wallet_address#d1735400 query_id:uint64 wallet_address:MsgAddress owner_address:(Maybe ^MsgAddress) = InternalMsgBody; + */ + const deployerJettonWallet = await userWallet(deployer.address); + expect(discoveryResult.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployer.address, + body: beginCell() + .storeUint(JettonMinter.opcodes.TakeWalletAddress, 32) + .storeUint(0, 64) + .storeAddress(deployerJettonWallet.address) + .storeUint(1, 1) + .storeRef( + beginCell().storeAddress(deployer.address).endCell(), + ) + .endCell(), + }); + + discoveryResult = await sendDiscovery( + jettonMinter, + deployer.getSender(), + notDeployer.address, + true, + ); + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + expect(discoveryResult.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployer.address, + body: beginCell() + .storeUint(JettonMinter.opcodes.TakeWalletAddress, 32) + .storeUint(0, 64) + .storeAddress(notDeployerJettonWallet.address) + .storeUint(1, 1) + .storeRef( + beginCell().storeAddress(notDeployer.address).endCell(), + ) + .endCell(), + }); + + // do not include owner address + discoveryResult = await sendDiscovery( + jettonMinter, + deployer.getSender(), + notDeployer.address, + false, + ); + expect(discoveryResult.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployer.address, + body: beginCell() + .storeUint(JettonMinter.opcodes.TakeWalletAddress, 32) + .storeUint(0, 64) + .storeAddress(notDeployerJettonWallet.address) + .storeUint(0, 1) + .endCell(), + }); + }); + + it("Correctly handles not valid address in discovery", async () => { + const { deployer, jettonMinter } = await setup(); + const badAddr = randomAddress(-1); + let discoveryResult = await sendDiscovery( + jettonMinter, + deployer.getSender(), + badAddr, + false, + ); + + expect(discoveryResult.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployer.address, + body: beginCell() + .storeUint(JettonMinter.opcodes.TakeWalletAddress, 32) + .storeUint(0, 64) + .storeUint(0, 2) // addr_none + .storeUint(0, 1) + .endCell(), + }); + + // Include address should still be available + + discoveryResult = await sendDiscovery( + jettonMinter, + deployer.getSender(), + badAddr, + true, + ); // Include addr + + expect(discoveryResult.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployer.address, + body: beginCell() + .storeUint(JettonMinter.opcodes.TakeWalletAddress, 32) + .storeUint(0, 64) + .storeUint(0, 2) // addr_none + .storeUint(1, 1) + .storeRef(beginCell().storeAddress(badAddr).endCell()) + .endCell(), + }); + }); + + it("Can send even giant payload", async () => { + const { blockchain, userWallet, deployer, notDeployer } = + await setup(); + const deployerJettonWallet = await userWallet(deployer.address); + const jwState = ( + await blockchain.getContract(deployerJettonWallet.address) + ).account; + const originalBalance = jwState.account!.storage.balance.coins; + + jwState.account!.storage.balance.coins = 0n; + await blockchain.setShardAccount( + deployerJettonWallet.address, + jwState, + ); + + const maxPayload = beginCell() + .storeUint(1, 1) // Store Either bit = 1, as we store payload in ref + .storeRef(storeBigPayload(beginCell()).endCell()) // Here we generate big payload, to cause high forward fee + .endCell(); + + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.2"), // Quite low amount, enough to cover one forward fee but not enough to cover two + 0n, + notDeployer.address, + notDeployer.address, + null, + 2n, // Forward ton amount, that causes bug, described below + maxPayload, + ); + + // Here we check, that the transaction should bounce on the first jetton wallet + // Or it should be fully completed + + // However, as we had incorrect logic of forward fee calculation, + // https://github.com/tact-lang/jetton/issues/58 + // Jetton version with that bug will not be able to send Jetton Notification + try { + // Expect that JettonNotify is sent + expect(sendResult.transactions).toHaveTransaction({ + from: (await userWallet(notDeployer.address)).address, + to: notDeployer.address, + success: true, + }); + } catch { + // OR that the transaction is bounced on the first jetton wallet + expect(sendResult.transactions).toHaveTransaction({ + on: deployerJettonWallet.address, + aborted: true, + }); + } + + jwState.account!.storage.balance.coins = originalBalance; // restore balance + await blockchain.setShardAccount( + deployerJettonWallet.address, + jwState, + ); + expect( + (await blockchain.getContract(deployerJettonWallet.address)) + .balance, + ).toEqual(originalBalance); + }); + + // This test consume a lot of time: 18 sec + // and is needed only for measuring ton accruing + /* it('jettonWallet can process 250 transfer', async () => { + const deployerJettonWallet = await userWallet(deployer.address); + let initialJettonBalance = await deployerJettonWallet.getJettonBalance(); + const notDeployerJettonWallet = await userWallet(notDeployer.address); + let initialJettonBalance2 = await notDeployerJettonWallet.getJettonBalance(); + let sentAmount = 1n, count = 250n; + let forwardAmount = toNano('0.05'); + let sendResult: any; + let payload = beginCell() + .storeUint(0x12345678, 32).storeUint(0x87654321, 32) + .storeRef(beginCell().storeUint(0x12345678, 32).storeUint(0x87654321, 108).endCell()) + .storeRef(beginCell().storeUint(0x12345671, 32).storeUint(0x87654321, 240).endCell()) + .storeRef(beginCell().storeUint(0x12345672, 32).storeUint(0x87654321, 77) + .storeRef(beginCell().endCell()) + .storeRef(beginCell().storeUint(0x1245671, 91).storeUint(0x87654321, 32).endCell()) + .storeRef(beginCell().storeUint(0x2245671, 180).storeUint(0x87654321, 32).endCell()) + .storeRef(beginCell().storeUint(0x8245671, 255).storeUint(0x87654321, 32).endCell()) + .endCell()) + .endCell(); + let initialBalance =(await blockchain.getContract(deployerJettonWallet.address)).balance; + let initialBalance2 = (await blockchain.getContract(notDeployerJettonWallet.address)).balance; + for(let i = 0; i < count; i++) { + sendResult = await deployerJettonWallet.sendTransferMessage(deployer.getSender(), toNano('0.1'), //tons + sentAmount, notDeployer.address, + deployer.address, null, forwardAmount, payload); + } + // last chain was successful + expect(sendResult.transactions).toHaveTransaction({ //excesses + from: notDeployerJettonWallet.address, + to: deployer.address, + }); + expect(sendResult.transactions).toHaveTransaction({ //notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + value: forwardAmount + }); + + expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance - sentAmount*count); + expect(await notDeployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance2 + sentAmount*count); + + let finalBalance =(await blockchain.getContract(deployerJettonWallet.address)).balance; + let finalBalance2 = (await blockchain.getContract(notDeployerJettonWallet.address)).balance; + + // if it is not true, it's ok but gas_consumption constant is too high + // and excesses of TONs will be accrued on wallet + expect(finalBalance).toBeLessThan(initialBalance + toNano('0.001')); + expect(finalBalance2).toBeLessThan(initialBalance2 + toNano('0.001')); + expect(finalBalance).toBeGreaterThan(initialBalance - toNano('0.001')); + expect(finalBalance2).toBeGreaterThan(initialBalance2 - toNano('0.001')); + + }); + */ + // implementation detail + it("can not send to masterchain", async () => { + const { userWallet, deployer } = await setup(); + + const deployerJettonWallet = await userWallet(deployer.address); + const sentAmount = toNano("0.5"); + const forwardAmount = toNano("0.05"); + const sendResult = await sendTransfer( + deployerJettonWallet, + deployer.getSender(), + toNano("0.2"), // tons + sentAmount, + Address.parse( + "Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU", + ), + deployer.address, + null, + forwardAmount, + null, + ); + expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: deployer.address, + to: deployerJettonWallet.address, + aborted: true, + exitCode: errors["Not a basechain address"], + }); + }); + }); +}; +export const testBounces = ( + fromInitMinter: FromInitMinter, + fromInitWallet: FromInitWallet, +) => { + const setup = async () => { + return await globalSetup(fromInitMinter, fromInitWallet); + }; + describe("Bounces", () => { + it("wallet should restore balance on internal_transfer bounce", async () => { + const { + userWallet, + deployer, + jettonMinter, + blockchain, + notDeployer, + } = await setup(); + const initRes = await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + 201n, + 0n, + toNano(1), + ); + const deployerJettonWallet = await userWallet(deployer.address); + expect(initRes.transactions).toHaveTransaction({ + from: jettonMinter.address, + to: deployerJettonWallet.address, + success: true, + }); + + const notDeployerJettonWallet = await userWallet( + notDeployer.address, + ); + const balanceBefore = await getJettonBalance(deployerJettonWallet); + const txAmount = BigInt(getRandomInt(100, 200)); + const transferMsg = beginCell() + .store( + storeJettonTransfer({ + $$type: "JettonTransfer", + queryId: 0n, + amount: txAmount, + responseDestination: deployer.address, + destination: notDeployer.address, + customPayload: null, + forwardTonAmount: 0n, + forwardPayload: beginCell().storeUint(0, 1).asSlice(), + }), + ) + .endCell(); + + const walletSmc = await blockchain.getContract( + deployerJettonWallet.address, + ); + + const res = await walletSmc.receiveMessage( + internal({ + from: deployer.address, + to: deployerJettonWallet.address, + body: transferMsg, + value: toNano("1"), + }), + ); + expect(res.outMessagesCount).toEqual(1); + const firstOutMsg = res.outMessages.get(0); + if (!firstOutMsg) { + throw new Error("No out message"); // It is impossible due to the check above + } + const outMsgSc = firstOutMsg.body.beginParse(); + expect(outMsgSc.preloadUint(32)).toEqual( + JettonMinter.opcodes.JettonTransferInternal, + ); + + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + balanceBefore - txAmount, + ); + + await walletSmc.receiveMessage( + internal({ + from: notDeployerJettonWallet.address, + to: walletSmc.address, + bounced: true, + body: beginCell() + .storeUint(0xffffffff, 32) + .storeSlice(outMsgSc) + .endCell(), + value: toNano("0.95"), + }), + ); + + // Balance should roll back + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + balanceBefore, + ); + }); + it("wallet should restore balance on burn_notification bounce", async () => { + const { userWallet, deployer, jettonMinter, blockchain } = + await setup(); + // Mint some jettons + await sendMint( + jettonMinter, + deployer.getSender(), + deployer.address, + 201n, + 0n, + toNano(1), + ); + const deployerJettonWallet = await userWallet(deployer.address); + const balanceBefore = await getJettonBalance(deployerJettonWallet); + const burnAmount = BigInt(getRandomInt(100, 200)); + + const burnMsg = beginCell() + .store( + storeJettonBurn({ + $$type: "JettonBurn", + queryId: 0n, + amount: burnAmount, + responseDestination: deployer.address, + customPayload: null, + }), + ) + .endCell(); + + const walletSmc = await blockchain.getContract( + deployerJettonWallet.address, + ); + + const res = await walletSmc.receiveMessage( + internal({ + from: deployer.address, + to: deployerJettonWallet.address, + body: burnMsg, + value: toNano("1"), + }), + ); + + expect(res.outMessagesCount).toEqual(1); + const firstOutMsg = res.outMessages.get(0); + if (!firstOutMsg) { + throw new Error("No out message"); // It is impossible due to the check above + } + const outMsgSc = firstOutMsg.body.beginParse(); + expect(outMsgSc.preloadUint(32)).toEqual( + JettonMinter.opcodes.JettonBurnNotification, + ); + + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + balanceBefore - burnAmount, + ); + + await walletSmc.receiveMessage( + internal({ + from: jettonMinter.address, + to: walletSmc.address, + bounced: true, + body: beginCell() + .storeUint(0xffffffff, 32) + .storeSlice(outMsgSc) + .endCell(), + value: toNano("0.95"), + }), + ); + + // Balance should roll back + expect(await getJettonBalance(deployerJettonWallet)).toEqual( + balanceBefore, + ); + }); + }); +}; diff --git a/src/benchmarks/nft/nft.spec.ts b/src/benchmarks/nft/bench.spec.ts similarity index 98% rename from src/benchmarks/nft/nft.spec.ts rename to src/benchmarks/nft/bench.spec.ts index 0ae5185c1e..149f34dfed 100644 --- a/src/benchmarks/nft/nft.spec.ts +++ b/src/benchmarks/nft/bench.spec.ts @@ -42,8 +42,8 @@ import { storeInitNFTBody, } from "@/benchmarks/nft/tact/output/collection_NFTItem"; -import benchmarkResults from "@/benchmarks/nft/results_gas.json"; -import benchmarkCodeSizeResults from "@/benchmarks/nft/results_code_size.json"; +import benchmarkResults from "@/benchmarks/nft/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/nft/size.json"; type dictDeployNFT = { amount: bigint; diff --git a/src/benchmarks/nft/results_gas.json b/src/benchmarks/nft/gas.json similarity index 100% rename from src/benchmarks/nft/results_gas.json rename to src/benchmarks/nft/gas.json diff --git a/src/benchmarks/nft/results_code_size.json b/src/benchmarks/nft/size.json similarity index 100% rename from src/benchmarks/nft/results_code_size.json rename to src/benchmarks/nft/size.json diff --git a/src/benchmarks/notcoin/notcoin.spec.ts b/src/benchmarks/notcoin/bench.spec.ts similarity index 98% rename from src/benchmarks/notcoin/notcoin.spec.ts rename to src/benchmarks/notcoin/bench.spec.ts index 275ca0cbe6..bafce533f5 100644 --- a/src/benchmarks/notcoin/notcoin.spec.ts +++ b/src/benchmarks/notcoin/bench.spec.ts @@ -35,8 +35,8 @@ import { type JettonBurn, } from "@/benchmarks/notcoin/tact/output/wallet_JettonWalletNotcoin"; -import benchmarkResults from "@/benchmarks/notcoin/results_gas.json"; -import benchmarkCodeSizeResults from "@/benchmarks/notcoin/results_code_size.json"; +import benchmarkResults from "@/benchmarks/notcoin/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/notcoin/size.json"; const loadNotcoinJettonsBoc = () => { const bocMinter = readFileSync( diff --git a/src/benchmarks/notcoin/results_gas.json b/src/benchmarks/notcoin/gas.json similarity index 100% rename from src/benchmarks/notcoin/results_gas.json rename to src/benchmarks/notcoin/gas.json diff --git a/src/benchmarks/notcoin/results_code_size.json b/src/benchmarks/notcoin/size.json similarity index 100% rename from src/benchmarks/notcoin/results_code_size.json rename to src/benchmarks/notcoin/size.json diff --git a/src/benchmarks/sbt/sbt.spec.ts b/src/benchmarks/sbt/bench.spec.ts similarity index 99% rename from src/benchmarks/sbt/sbt.spec.ts rename to src/benchmarks/sbt/bench.spec.ts index 8a58517dc4..d7f9a917b7 100644 --- a/src/benchmarks/sbt/sbt.spec.ts +++ b/src/benchmarks/sbt/bench.spec.ts @@ -38,8 +38,8 @@ import type { ExcessOut, } from "@/benchmarks/sbt/tact/output/item_SBTItem"; -import benchmarkResults from "@/benchmarks/sbt/results_gas.json"; -import benchmarkCodeSizeResults from "@/benchmarks/sbt/results_code_size.json"; +import benchmarkResults from "@/benchmarks/sbt/gas.json"; +import benchmarkCodeSizeResults from "@/benchmarks/sbt/size.json"; const loadFunCSBTBoc = () => { const bocItem = readFileSync( diff --git a/src/benchmarks/sbt/results_gas.json b/src/benchmarks/sbt/gas.json similarity index 100% rename from src/benchmarks/sbt/results_gas.json rename to src/benchmarks/sbt/gas.json diff --git a/src/benchmarks/sbt/results_code_size.json b/src/benchmarks/sbt/size.json similarity index 100% rename from src/benchmarks/sbt/results_code_size.json rename to src/benchmarks/sbt/size.json diff --git a/src/benchmarks/update.build.ts b/src/benchmarks/update.build.ts index 013c18768d..1cd0ece75a 100644 --- a/src/benchmarks/update.build.ts +++ b/src/benchmarks/update.build.ts @@ -216,7 +216,7 @@ const updateCodeSizeResultsFile = async ( const main = async () => { try { - const benchmarkPaths = globSync(["**/*.spec.ts"], { + const benchmarkPaths = globSync(["**/bench.spec.ts"], { cwd: __dirname, }); @@ -237,12 +237,8 @@ const main = async () => { return; } - const resultsGas = join(specPath, "..", "results_gas.json"); - const resultsCodeSize = join( - specPath, - "..", - "results_code_size.json", - ); + const resultsGas = join(specPath, "..", "gas.json"); + const resultsCodeSize = join(specPath, "..", "size.json"); const isUpdate = typeof process.env.ADD !== "undefined"; diff --git a/src/benchmarks/wallet-v4/wallet-v4.spec.ts b/src/benchmarks/wallet-v4/bench.spec.ts similarity index 99% rename from src/benchmarks/wallet-v4/wallet-v4.spec.ts rename to src/benchmarks/wallet-v4/bench.spec.ts index 00ad4530f4..b44c6c079c 100644 --- a/src/benchmarks/wallet-v4/wallet-v4.spec.ts +++ b/src/benchmarks/wallet-v4/bench.spec.ts @@ -37,7 +37,7 @@ import { validUntil, } from "@/benchmarks/wallet-v5/utils"; -import benchmarkResults from "@/benchmarks/wallet-v4/results_gas.json"; +import benchmarkResults from "@/benchmarks/wallet-v4/gas.json"; function createSimpleTransferBody(testReceiver: Address, forwardValue: bigint) { const msg = beginCell().storeUint(0, 8); diff --git a/src/benchmarks/wallet-v4/results_gas.json b/src/benchmarks/wallet-v4/gas.json similarity index 100% rename from src/benchmarks/wallet-v4/results_gas.json rename to src/benchmarks/wallet-v4/gas.json diff --git a/src/benchmarks/wallet-v5/wallet-v5.spec.ts b/src/benchmarks/wallet-v5/bench.spec.ts similarity index 99% rename from src/benchmarks/wallet-v5/wallet-v5.spec.ts rename to src/benchmarks/wallet-v5/bench.spec.ts index 23b55c8030..89f8cb6459 100644 --- a/src/benchmarks/wallet-v5/wallet-v5.spec.ts +++ b/src/benchmarks/wallet-v5/bench.spec.ts @@ -32,7 +32,7 @@ import { validUntil, } from "@/benchmarks/wallet-v5/utils"; -import benchmarkResults from "@/benchmarks/wallet-v5/results_gas.json"; +import benchmarkResults from "@/benchmarks/wallet-v5/gas.json"; import { WalletV5 } from "@/benchmarks/wallet-v5/tact/output/wallet-v5_WalletV5"; export function packAddress(address: Address) { diff --git a/src/benchmarks/wallet-v5/results_gas.json b/src/benchmarks/wallet-v5/gas.json similarity index 100% rename from src/benchmarks/wallet-v5/results_gas.json rename to src/benchmarks/wallet-v5/gas.json diff --git a/src/benchmarks/tests/wallet-v5-test.spec.ts b/src/benchmarks/wallet-v5/test.spec.ts similarity index 100% rename from src/benchmarks/tests/wallet-v5-test.spec.ts rename to src/benchmarks/wallet-v5/test.spec.ts