@ton/sandbox 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +104 -0
  3. package/dist/blockchain/Blockchain.d.ts +16 -2
  4. package/dist/blockchain/Blockchain.js +31 -13
  5. package/dist/blockchain/SmartContract.js +2 -0
  6. package/dist/executor/Executor.d.ts +14 -0
  7. package/dist/executor/Executor.js +34 -1
  8. package/dist/executor/emulator-emscripten.js +1 -1
  9. package/dist/executor/emulator-emscripten.wasm.js +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +15 -0
  12. package/dist/jest/BenchmarkCommand.d.ts +10 -0
  13. package/dist/jest/BenchmarkCommand.js +14 -0
  14. package/dist/jest/BenchmarkEnvironment.d.ts +19 -0
  15. package/dist/jest/BenchmarkEnvironment.js +46 -0
  16. package/dist/jest/BenchmarkReporter.d.ts +45 -0
  17. package/dist/jest/BenchmarkReporter.js +207 -0
  18. package/dist/meta/ContractsMeta.d.ts +10 -0
  19. package/dist/meta/ContractsMeta.js +2 -0
  20. package/dist/metric/ContractDatabase.d.ts +21 -0
  21. package/dist/metric/ContractDatabase.js +111 -0
  22. package/dist/metric/collectMetric.d.ts +106 -0
  23. package/dist/metric/collectMetric.js +217 -0
  24. package/dist/metric/defaultColor.d.ts +2 -0
  25. package/dist/metric/defaultColor.js +45 -0
  26. package/dist/metric/deltaResult.d.ts +39 -0
  27. package/dist/metric/deltaResult.js +210 -0
  28. package/dist/metric/gasReportTable.d.ts +2 -0
  29. package/dist/metric/gasReportTable.js +112 -0
  30. package/dist/metric/index.d.ts +6 -0
  31. package/dist/metric/index.js +22 -0
  32. package/dist/metric/readSnapshots.d.ts +2 -0
  33. package/dist/metric/readSnapshots.js +33 -0
  34. package/dist/utils/readJsonl.d.ts +1 -0
  35. package/dist/utils/readJsonl.js +25 -0
  36. package/jest-environment.js +1 -0
  37. package/jest-reporter.js +1 -0
  38. package/package.json +24 -5
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.32.0] - 2025-06-02
9
+
10
+ ### Added
11
+
12
+ - Added contract meta gathering via meta field in blockchain
13
+ - Added ability to set `PREVBLOCKS` information
14
+
15
+ ## [0.31.0] - 2025-05-20
16
+
17
+ ### Added
18
+
19
+ - Added methods to collect metrics of contracts
20
+ - Added `@ton/sandbox/jest-environment` and `@ton/sandbox/jest-reporter` to write metric snapshots from test run results
21
+ - Added contract method ABI auto-mapping mechanism for detailed benchmark metrics
22
+ - Added methods to generate delta reports from metrics of contracts
23
+
8
24
  ## [0.30.0] - 2025-05-12
9
25
 
10
26
  ### Changed
@@ -383,4 +399,4 @@ type LogsVerbosity = {
383
399
 
384
400
  ### Removed
385
401
 
386
- - Changed `blockchain.pushMessage`, `blockchain.processQueue`, `blockchain.runQueue` to be private
402
+ - Changed `blockchain.pushMessage`, `blockchain.processQueue`, `blockchain.runQueue` to be private
package/README.md CHANGED
@@ -15,6 +15,7 @@ The key difference of this package from [ton-contract-executor](https://github.c
15
15
  * [Cross contract tests](#cross-contract-tests)
16
16
  * [Testing key points](#testing-key-points)
17
17
  * [Test examples](#test-examples)
18
+ * [Benchmark contracts](#benchmark-contracts)
18
19
  * [Sandbox pitfalls](#sandbox-pitfalls)
19
20
  * [Viewing logs](#viewing-logs)
20
21
  * [Setting smart contract state directly](#setting-smart-contract-state-directly)
@@ -290,6 +291,109 @@ Learn more from examples:
290
291
  * [FunC Test Examples](https://docs.ton.org/develop/smart-contracts/examples#examples-of-tests-for-smart-contracts)
291
292
  * [Tact Test Examples](docs/tact-testing-examples.md)
292
293
 
294
+ ## Benchmark contracts
295
+
296
+ The `@ton/sandbox` package provides `@ton/sandbox/jest-environment` and `@ton/sandbox/jest-reporter` built-in support for benchmarking smart contract behavior during tests, including tracking gas usage, cell size, opcode execution, and action phases.
297
+ This is especially useful for performance analysis, gas optimization, and regression checks on contract logic.
298
+
299
+ > ℹ️ See also: [Collect metric API](docs/collect-metric-api.md) for low-level control and manual snapshots.
300
+
301
+ ### Features
302
+
303
+ * Automatic metric collection from all transactions triggered during tests
304
+ * Snapshot reporting with contract-level filters
305
+ * Integration with [blueprint](https://github.com/ton-org/blueprint#benchmark-contracts)
306
+
307
+ ### Setup in `jest.config.ts`
308
+
309
+ ```ts
310
+ import type { Config } from 'jest';
311
+
312
+ const config: Config = {
313
+ preset: 'ts-jest',
314
+ testEnvironment: '@ton/sandbox/jest-environment',
315
+ globalSetup: './jest.setup.ts',
316
+ testPathIgnorePatterns: ['/node_modules/', '/dist/'],
317
+ reporters: [
318
+ 'default',
319
+ ['@ton/sandbox/jest-reporter', {
320
+ // options
321
+ snapshotDir: '.snapshot', // output folder for benchmark reports, default: '.snapshot'
322
+ contractDatabase: 'contract.abi.json', // path or json a map of known contracts, see Collect metric API, default: 'contract.abi.json'
323
+ reportName: 'gas-report', // report name, default: 'gas-report'
324
+ depthCompare: 2, // comparison depth, default: 2
325
+ removeRawResult: true, // remove raw metric file, default: true
326
+ contractExcludes: [ // exclude specific contracts from snapshot, default: []
327
+ 'TreasuryContract',
328
+ ],
329
+ }],
330
+ ],
331
+ };
332
+
333
+ export default config;
334
+ ```
335
+
336
+ ### How to run benchmarks
337
+
338
+ To collect and save snapshot metrics:
339
+
340
+ ```bash
341
+ BENCH_NEW="some" npx jest
342
+ # or
343
+ npx blueprint snapshot --label "some"
344
+ ```
345
+
346
+ This will:
347
+
348
+ * Run your tests
349
+ * Collect contract execution metrics
350
+ * Save a snapshot in `.snapshot/<timestamp>.json`
351
+
352
+ To compare with a previous snapshot:
353
+
354
+ ```bash
355
+ BENCH_DIFF=true npx jest
356
+ # or
357
+ npx blueprint test --gas-report
358
+ ```
359
+
360
+ ### Or setup in `gas-report.config.ts`
361
+
362
+ ```ts
363
+ import config from './jest.config';
364
+
365
+ config.testNamePattern = '^DescribeName .* - test name$'
366
+ config.testEnvironment = '@ton/sandbox/jest-environment'
367
+ config.reporters = [
368
+ ['@ton/sandbox/jest-reporter', {
369
+ contractDatabase: 'abi.json',
370
+ contractExcludes: [
371
+ 'TreasuryContract',
372
+ ],
373
+ }],
374
+ ]
375
+ export default config;
376
+ ```
377
+
378
+ **Collect metric and get report:**
379
+
380
+ ```bash
381
+ npx blueprint snapshot --label "some label" -- --config gas-report.config.ts
382
+ npx blueprint test --gas-report -- --config gas-report.config.ts
383
+ ```
384
+
385
+ ### Output structure
386
+
387
+ By default, the reporter generates:
388
+
389
+ ```
390
+ .project-root/
391
+ ├── .snapshot/
392
+ │ └── 4200000000000.json // timestamped snapshot file
393
+ ├── .sandbox-metric-raw.jsonl // raw metric log (auto-deleted by default)
394
+ ├── contract.abi.json // map of known contracts, see Collect metric API
395
+ └── gas-report.json // aggregate report in json format
396
+ ```
293
397
 
294
398
  ## Sandbox pitfalls
295
399
 
@@ -1,11 +1,12 @@
1
1
  import { Address, Cell, Message, Transaction, ContractProvider, Contract, Sender, ShardAccount, TupleItem, ExternalAddress, StateInit, OpenedContract } from "@ton/core";
2
- import { IExecutor, TickOrTock } from "../executor/Executor";
2
+ import { IExecutor, TickOrTock, PrevBlocksInfo } from "../executor/Executor";
3
3
  import { BlockchainStorage } from "./BlockchainStorage";
4
4
  import { Event } from "../event/Event";
5
5
  import { SandboxContractProvider } from "./BlockchainContractProvider";
6
6
  import { TreasuryContract } from "../treasury/Treasury";
7
7
  import { GetMethodParams, LogsVerbosity, MessageParams, SmartContract, SmartContractSnapshot, Verbosity } from "./SmartContract";
8
8
  import { AsyncLock } from "../utils/AsyncLock";
9
+ import { ContractsMeta } from "../meta/ContractsMeta";
9
10
  export type ExternalOutInfo = {
10
11
  type: 'external-out';
11
12
  src: Address;
@@ -101,6 +102,8 @@ export declare class Blockchain {
101
102
  protected contractFetches: Map<string, Promise<SmartContract>>;
102
103
  protected nextCreateWalletIndex: number;
103
104
  protected shouldRecordStorage: boolean;
105
+ protected meta?: ContractsMeta;
106
+ protected prevBlocksInfo?: PrevBlocksInfo;
104
107
  readonly executor: IExecutor;
105
108
  /**
106
109
  * Saves snapshot of current blockchain.
@@ -143,6 +146,7 @@ export declare class Blockchain {
143
146
  executor: IExecutor;
144
147
  config?: BlockchainConfig;
145
148
  storage: BlockchainStorage;
149
+ meta?: ContractsMeta;
146
150
  });
147
151
  /**
148
152
  * @returns Config used in blockchain.
@@ -152,6 +156,15 @@ export declare class Blockchain {
152
156
  * @returns Config used in blockchain in base64 format.
153
157
  */
154
158
  get configBase64(): string;
159
+ /**
160
+ * @returns Current PrevBlocksInfo
161
+ */
162
+ get prevBlocks(): PrevBlocksInfo | undefined;
163
+ /**
164
+ * Sets PrevBlocksInfo.
165
+ * @param value PrevBlocksInfo to set
166
+ */
167
+ set prevBlocks(value: PrevBlocksInfo | undefined);
155
168
  /**
156
169
  * Emulates the result of sending a message to this Blockchain. Emulates the whole chain of transactions before returning the result. Each transaction increases lt by 1000000.
157
170
  *
@@ -329,7 +342,7 @@ export declare class Blockchain {
329
342
  * @param [opts.executor] Custom contract executor. If omitted {@link Executor} is used.
330
343
  * @param [opts.config] Config used in blockchain. If omitted {@link defaultConfig} is used.
331
344
  * @param [opts.storage] Contracts storage used for blockchain. If omitted {@link LocalBlockchainStorage} is used.
332
- *
345
+ * @param [opts.meta] Optional contracts metadata provider. If not provided, {@link @ton/test-utils.contractsMeta} will be used to accumulate contracts metadata.
333
346
  * @example
334
347
  * const blockchain = await Blockchain.create({ config: 'slim' });
335
348
  *
@@ -346,6 +359,7 @@ export declare class Blockchain {
346
359
  executor?: IExecutor;
347
360
  config?: BlockchainConfig;
348
361
  storage?: BlockchainStorage;
362
+ meta?: ContractsMeta;
349
363
  }): Promise<Blockchain>;
350
364
  }
351
365
  export {};
@@ -13,6 +13,7 @@ const AsyncLock_1 = require("../utils/AsyncLock");
13
13
  const message_1 = require("../utils/message");
14
14
  const slimConfig_1 = require("../config/slimConfig");
15
15
  const testTreasurySubwalletId_1 = require("../utils/testTreasurySubwalletId");
16
+ const collectMetric_1 = require("../metric/collectMetric");
16
17
  const CREATE_WALLETS_PREFIX = 'CREATE_WALLETS';
17
18
  function createWalletsSeed(idx) {
18
19
  return `${CREATE_WALLETS_PREFIX}${idx}`;
@@ -129,6 +130,7 @@ class Blockchain {
129
130
  this.networkConfig = blockchainConfigToBase64(opts.config);
130
131
  this.executor = opts.executor;
131
132
  this.storage = opts.storage;
133
+ this.meta = opts.meta;
132
134
  }
133
135
  /**
134
136
  * @returns Config used in blockchain.
@@ -142,6 +144,19 @@ class Blockchain {
142
144
  get configBase64() {
143
145
  return this.networkConfig;
144
146
  }
147
+ /**
148
+ * @returns Current PrevBlocksInfo
149
+ */
150
+ get prevBlocks() {
151
+ return this.prevBlocksInfo;
152
+ }
153
+ /**
154
+ * Sets PrevBlocksInfo.
155
+ * @param value PrevBlocksInfo to set
156
+ */
157
+ set prevBlocks(value) {
158
+ this.prevBlocksInfo = value;
159
+ }
145
160
  /**
146
161
  * Emulates the result of sending a message to this Blockchain. Emulates the whole chain of transactions before returning the result. Each transaction increases lt by 1000000.
147
162
  *
@@ -400,6 +415,7 @@ class Blockchain {
400
415
  else if ((params?.resetBalanceIfZero ?? true) && contract.balance === 0n) {
401
416
  contract.balance = params?.balance ?? (0, core_1.toNano)(TREASURY_INIT_BALANCE_TONS);
402
417
  }
418
+ this.meta?.upsert(wallet.address, { treasurySeed: seed });
403
419
  return wallet;
404
420
  }
405
421
  /**
@@ -443,6 +459,7 @@ class Blockchain {
443
459
  }
444
460
  init = contract.init;
445
461
  }
462
+ this.meta?.upsert(address, { wrapperName: contract?.constructor?.name, abi: contract.abi });
446
463
  const provider = this.provider(address, init);
447
464
  const blkch = this;
448
465
  return new Proxy(contract, {
@@ -452,25 +469,25 @@ class Blockchain {
452
469
  }
453
470
  const value = target[prop];
454
471
  if (typeof prop === 'string' && typeof value === 'function') {
472
+ const ctx = {
473
+ contract,
474
+ methodName: prop,
475
+ };
455
476
  if (prop.startsWith('get')) {
456
477
  return (...args) => value.apply(target, [provider, ...args]);
457
478
  }
458
479
  else if (prop.startsWith('send')) {
459
480
  return async (...args) => {
460
- const ret = value.apply(target, [provider, ...args]);
481
+ let ret = value.apply(target, [provider, ...args]);
461
482
  if (ret instanceof Promise) {
462
- const r = await ret;
463
- return {
464
- ...await blkch.runQueue(),
465
- result: r,
466
- };
467
- }
468
- else {
469
- return {
470
- ...await blkch.runQueue(),
471
- result: ret,
472
- };
483
+ ret = await ret;
473
484
  }
485
+ const out = {
486
+ ...await blkch.runQueue(),
487
+ result: ret,
488
+ };
489
+ await (0, collectMetric_1.collectMetric)(blkch, ctx, out);
490
+ return out;
474
491
  };
475
492
  }
476
493
  }
@@ -564,7 +581,7 @@ class Blockchain {
564
581
  * @param [opts.executor] Custom contract executor. If omitted {@link Executor} is used.
565
582
  * @param [opts.config] Config used in blockchain. If omitted {@link defaultConfig} is used.
566
583
  * @param [opts.storage] Contracts storage used for blockchain. If omitted {@link LocalBlockchainStorage} is used.
567
- *
584
+ * @param [opts.meta] Optional contracts metadata provider. If not provided, {@link @ton/test-utils.contractsMeta} will be used to accumulate contracts metadata.
568
585
  * @example
569
586
  * const blockchain = await Blockchain.create({ config: 'slim' });
570
587
  *
@@ -581,6 +598,7 @@ class Blockchain {
581
598
  return new Blockchain({
582
599
  executor: opts?.executor ?? await Executor_1.Executor.create(),
583
600
  storage: opts?.storage ?? new BlockchainStorage_1.LocalBlockchainStorage(),
601
+ meta: opts?.meta ?? require('@ton/test-utils')?.contractsMeta,
584
602
  ...opts
585
603
  });
586
604
  }
@@ -214,6 +214,7 @@ class SmartContract {
214
214
  randomSeed: params?.randomSeed ?? Buffer.alloc(32),
215
215
  ignoreChksig: params?.ignoreChksig ?? false,
216
216
  debugEnabled: this.verbosity.debugLogs,
217
+ prevBlocksInfo: this.blockchain.prevBlocks,
217
218
  };
218
219
  }
219
220
  async receiveMessage(message, params) {
@@ -287,6 +288,7 @@ class SmartContract {
287
288
  gasLimit: params?.gasLimit ?? 10000000n,
288
289
  debugEnabled: this.verbosity.debugLogs,
289
290
  extraCurrency: this.ec,
291
+ prevBlocksInfo: this.blockchain.prevBlocks,
290
292
  });
291
293
  if (this.verbosity.print && this.verbosity.blockchainLogs && res.logs.length > 0) {
292
294
  console.log(res.logs);
@@ -1,6 +1,18 @@
1
1
  /// <reference types="node" />
2
2
  import { Address, Cell, TupleItem } from "@ton/core";
3
3
  import { ExtraCurrency } from "../utils/ec";
4
+ export type BlockId = {
5
+ workchain: number;
6
+ shard: bigint;
7
+ seqno: number;
8
+ rootHash: Buffer;
9
+ fileHash: Buffer;
10
+ };
11
+ export type PrevBlocksInfo = {
12
+ lastMcBlocks: BlockId[];
13
+ prevKeyBlock: BlockId;
14
+ lastMcBlocks100?: BlockId[];
15
+ };
4
16
  export type GetMethodArgs = {
5
17
  code: Cell;
6
18
  data: Cell;
@@ -16,6 +28,7 @@ export type GetMethodArgs = {
16
28
  gasLimit: bigint;
17
29
  debugEnabled: boolean;
18
30
  extraCurrency?: ExtraCurrency;
31
+ prevBlocksInfo?: PrevBlocksInfo;
19
32
  };
20
33
  export type GetMethodResultSuccess = {
21
34
  success: true;
@@ -44,6 +57,7 @@ export type RunCommonArgs = {
44
57
  randomSeed: Buffer | null;
45
58
  ignoreChksig: boolean;
46
59
  debugEnabled: boolean;
60
+ prevBlocksInfo?: PrevBlocksInfo;
47
61
  };
48
62
  export type RunTransactionArgs = {
49
63
  message: Cell;
@@ -4,6 +4,32 @@ exports.Executor = void 0;
4
4
  const core_1 = require("@ton/core");
5
5
  const base64_1 = require("../utils/base64");
6
6
  const EmulatorModule = require('./emulator-emscripten.js');
7
+ function blockIdToTuple(blockId) {
8
+ return [
9
+ { type: 'int', value: BigInt(blockId.workchain) },
10
+ { type: 'int', value: blockId.shard },
11
+ { type: 'int', value: BigInt(blockId.seqno) },
12
+ { type: 'int', value: BigInt('0x' + blockId.rootHash.toString('hex')) },
13
+ { type: 'int', value: BigInt('0x' + blockId.fileHash.toString('hex')) },
14
+ ];
15
+ }
16
+ function prevBlocksInfoToTuple(prevBlocksInfo) {
17
+ const r = [
18
+ { type: 'tuple', items: prevBlocksInfo.lastMcBlocks.map(bid => ({ type: 'tuple', items: blockIdToTuple(bid) })) },
19
+ { type: 'tuple', items: blockIdToTuple(prevBlocksInfo.prevKeyBlock) },
20
+ ];
21
+ if (prevBlocksInfo.lastMcBlocks100) {
22
+ r.push({ type: 'tuple', items: prevBlocksInfo.lastMcBlocks100.map(bid => ({ type: 'tuple', items: blockIdToTuple(bid) })) });
23
+ }
24
+ return r;
25
+ }
26
+ function serializeTupleAsStackEntry(tuple) {
27
+ const c = (0, core_1.serializeTuple)([{ type: 'tuple', items: tuple }]);
28
+ const s = c.beginParse();
29
+ s.skip(24);
30
+ s.loadRef();
31
+ return s.asCell();
32
+ }
7
33
  const verbosityToNum = {
8
34
  'short': 0,
9
35
  'full': 1,
@@ -13,13 +39,17 @@ const verbosityToNum = {
13
39
  'full_location_stack_verbose': 5,
14
40
  };
15
41
  function runCommonArgsToInternalParams(args) {
16
- return {
42
+ const p = {
17
43
  utime: args.now,
18
44
  lt: args.lt.toString(),
19
45
  rand_seed: args.randomSeed === null ? '' : args.randomSeed.toString('hex'),
20
46
  ignore_chksig: args.ignoreChksig,
21
47
  debug_enabled: args.debugEnabled,
22
48
  };
49
+ if (args.prevBlocksInfo !== undefined) {
50
+ p.prev_blocks_info = serializeTupleAsStackEntry(prevBlocksInfoToTuple(args.prevBlocksInfo)).toBoc().toString('base64');
51
+ }
52
+ return p;
23
53
  }
24
54
  class Pointer {
25
55
  constructor(length, rawPointer) {
@@ -99,6 +129,9 @@ class Executor {
99
129
  params.extra_currencies[k] = v.toString();
100
130
  }
101
131
  }
132
+ if (args.prevBlocksInfo !== undefined) {
133
+ params.prev_blocks_info = serializeTupleAsStackEntry(prevBlocksInfoToTuple(args.prevBlocksInfo)).toBoc().toString('base64');
134
+ }
102
135
  let stack = (0, core_1.serializeTuple)(args.stack);
103
136
  this.debugLogs = [];
104
137
  const resp = JSON.parse(this.extractString(this.invoke('_run_get_method', [