From 13947038e8d4903ac825bd937db4c04c258abb4b Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 17:11:38 +0400 Subject: [PATCH 1/8] feat(core): add TON fee estimation module with blockchain-verified tests --- packages/core/package.json | 4 +- .../ton-blockchain/fee/__tests__/fees.spec.ts | 567 +++++++++++ .../__tests__/fixtures/blockchain-config.ts | 34 + .../fee/__tests__/fixtures/tonapi-fetcher.ts | 209 ++++ .../fee/__tests__/fixtures/utils.ts | 204 ++++ .../fixtures/v3r1-deploy-transfer.ts | 41 + .../__tests__/fixtures/v3r1-multi-transfer.ts | 43 + .../fixtures/v3r1-simple-transfer.ts | 38 + .../fixtures/v3r2-deploy-transfer.ts | 39 + .../__tests__/fixtures/v3r2-multi-transfer.ts | 41 + .../fixtures/v3r2-simple-transfer.ts | 38 + .../fixtures/v4r2-deploy-transfer.ts | 39 + .../__tests__/fixtures/v4r2-multi-transfer.ts | 41 + .../fixtures/v4r2-simple-transfer.ts | 38 + .../fixtures/v5r1-dedup-cross-msg.ts | 45 + .../fixtures/v5r1-dedup-within-msg.ts | 43 + .../fixtures/v5r1-deploy-transfer.ts | 39 + .../fixtures/v5r1-extension-add-eighth.ts | 63 ++ .../fixtures/v5r1-extension-add-first.ts | 46 + .../fixtures/v5r1-extension-add-ninth.ts | 61 ++ .../fixtures/v5r1-extension-add-second.ts | 52 + .../fixtures/v5r1-jetton-deploy-transfer.ts | 40 + .../fixtures/v5r1-jetton-simple-transfer.ts | 40 + .../__tests__/fixtures/v5r1-library-body.ts | 47 + .../__tests__/fixtures/v5r1-multi-transfer.ts | 41 + .../fixtures/v5r1-remove-ext-fork-sibling.ts | 67 ++ .../fixtures/v5r1-remove-ext-last.ts | 57 ++ .../fixtures/v5r1-remove-ext-leaf-sibling.ts | 66 ++ .../fixtures/v5r1-remove-ext-prelast.ts | 63 ++ .../fixtures/v5r1-send-all-transfer.ts | 41 + .../fixtures/v5r1-simple-transfer.ts | 38 + .../src/service/ton-blockchain/fee/compat.ts | 54 + .../src/service/ton-blockchain/fee/fees.ts | 913 +++++++++++++++++ .../src/service/ton-blockchain/fee/index.ts | 38 + packages/core/vitest.config.ts | 8 + yarn.lock | 939 +++++++++++++++++- 36 files changed, 4168 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/compat.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/fees.ts create mode 100644 packages/core/src/service/ton-blockchain/fee/index.ts create mode 100644 packages/core/vitest.config.ts diff --git a/packages/core/package.json b/packages/core/package.json index 420520c79..7738f29c7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,13 +13,15 @@ "generate:swapsApi": "rm -r ./src/swapsApi || true && npx openapi-typescript-codegen --input 'https://raw.githubusercontent.com/tonkeeper/swaps-backend/master/swagger.yaml?token=GHSAT0AAAAAACJYQUODBKR67AB7WULZBFWEZSUUGFQ' --output ./src/swapsApi", "generate:batteryApi": "rm -fr ./src/batteryApi && docker build --no-cache --build-arg GITHUB_TOKEN=GHSAT0AAAAAACJYQUODBULKMUQJQTJ7E7ES2H3LZ7A -f resource/Dockerfile.batteryApi . -t batteryapi && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local batteryapi", "generate:2faApi": "rm -fr src/2faApi && docker build -f resource/Dockerfile.2faApi . -t 2faapi && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local 2faapi", + "test": "vitest", "build:pkg": "yarn build", "build:analytics": "ts-node --project ./tsconfig.task.json ./task/build-analytics.ts" }, "devDependencies": { "@types/aes-js": "^3.1.4", "@types/punycode": "^2", - "typescript": "^4.9.4" + "typescript": "^4.9.4", + "vitest": "^4.0.18" }, "dependencies": { "@keystonehq/keystone-sdk": "0.7.2", diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts new file mode 100644 index 000000000..cf5da0d7b --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -0,0 +1,567 @@ +import { Cell } from '@ton/core'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import { BLOCKCHAIN_CONFIG_2024_12 } from './fixtures/blockchain-config'; +import { fetchExpectedFees, shouldFetchRealFees, ExpectedFees } from './fixtures/tonapi-fetcher'; +import { WalletFeeTestCase, parseWalletOutMsgCells } from './fixtures/utils'; +import { V3R1_DEPLOY_TRANSFER } from './fixtures/v3r1-deploy-transfer'; +import { V3R1_MULTI_TRANSFER } from './fixtures/v3r1-multi-transfer'; +import { V3R1_SIMPLE_TRANSFER } from './fixtures/v3r1-simple-transfer'; +import { V3R2_DEPLOY_TRANSFER } from './fixtures/v3r2-deploy-transfer'; +import { V3R2_MULTI_TRANSFER } from './fixtures/v3r2-multi-transfer'; +import { V3R2_SIMPLE_TRANSFER } from './fixtures/v3r2-simple-transfer'; +import { V4R2_DEPLOY_TRANSFER } from './fixtures/v4r2-deploy-transfer'; +import { V4R2_MULTI_TRANSFER } from './fixtures/v4r2-multi-transfer'; +import { V4R2_SIMPLE_TRANSFER } from './fixtures/v4r2-simple-transfer'; +import { V5R1_DEDUP_CROSS_MSG } from './fixtures/v5r1-dedup-cross-msg'; +import { V5R1_DEDUP_WITHIN_MSG } from './fixtures/v5r1-dedup-within-msg'; +import { V5R1_DEPLOY_TRANSFER } from './fixtures/v5r1-deploy-transfer'; +import { V5R1_EXTENSION_ADD_EIGHTH } from './fixtures/v5r1-extension-add-eighth'; +import { V5R1_EXTENSION_ADD_FIRST } from './fixtures/v5r1-extension-add-first'; +import { V5R1_EXTENSION_ADD_NINTH } from './fixtures/v5r1-extension-add-ninth'; +import { V5R1_EXTENSION_ADD_SECOND } from './fixtures/v5r1-extension-add-second'; +import { V5R1_JETTON_DEPLOY_TRANSFER } from './fixtures/v5r1-jetton-deploy-transfer'; +import { V5R1_JETTON_SIMPLE_TRANSFER } from './fixtures/v5r1-jetton-simple-transfer'; +import { V5R1_LIBRARY_BODY } from './fixtures/v5r1-library-body'; +import { V5R1_MULTI_TRANSFER } from './fixtures/v5r1-multi-transfer'; +import { V5R1_REMOVE_EXT_FORK_SIBLING } from './fixtures/v5r1-remove-ext-fork-sibling'; +import { V5R1_REMOVE_EXT_LAST } from './fixtures/v5r1-remove-ext-last'; +import { V5R1_REMOVE_EXT_LEAF_SIBLING } from './fixtures/v5r1-remove-ext-leaf-sibling'; +import { V5R1_REMOVE_EXT_PRELAST } from './fixtures/v5r1-remove-ext-prelast'; +import { V5R1_SEND_ALL_TRANSFER } from './fixtures/v5r1-send-all-transfer'; +import { V5R1_SIMPLE_TRANSFER } from './fixtures/v5r1-simple-transfer'; +import { + computeActionFee, + computeAddExtensionGas, + computeAddExtensionGasFromExtensions, + computeAddFirstExtensionGas, + computeForwardFee, + computeGasFee, + computeImportFee, + computeRemoveExtensionGas, + computeRemoveExtensionGasFromExtensions, + computeRemoveLastExtensionGas, + computeStorageFee, + computeWalletGasUsed, + estimateWalletFee, + EstimateWalletFeeParams, + extractFeeConfig, + parseV5R1ExtensionAction, + sumRefsStats +} from '../fees'; +import { TonWalletVersion } from '../compat'; + +/** + * TON Fee Calculation Specification + * + * This file serves as executable documentation for TON fee estimation. + * Each section documents a specific formula with unit tests. + * + * Modes: + * pnpm -F @tonkeeper/core test run fees.spec.ts # compare with fixtures + * FETCH_REAL_FEES=1 pnpm -F @tonkeeper/core test run fees.spec.ts # fetch from blockchain + */ + +// Get basechain config for unit tests +const unitTestConfig = extractFeeConfig(BLOCKCHAIN_CONFIG_2024_12, 0); + +// ============================================================================ +// 1. computeGasFee +// ============================================================================ + +describe('1. computeGasFee (formula: gasUsed × gasPrice >> 16)', () => { + it('returns 0 for gasUsed = 0', () => { + expect(computeGasFee(unitTestConfig, 0n)).toBe(0n); + }); + + it('calculates gas fee: gasUsed=4939 → 1975600', () => { + // V5R1 simple transfer: 4939 gas units + expect(computeGasFee(unitTestConfig, 4939n)).toBe(1975600n); + }); +}); + +// ============================================================================ +// 2. computeStorageFee +// ============================================================================ + +describe('2. computeStorageFee', () => { + /** + * Formula: ceil((bits × bitPrice + cells × cellPrice) × timeDelta / 2^16) + * Returns 0 when timeDelta <= 0 + */ + + describe('when timeDelta <= 0', () => { + it('returns 0 for timeDelta = 0', () => { + expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, 0n)).toBe(0n); + }); + + it('returns 0 for negative timeDelta', () => { + expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, -100n)).toBe(0n); + }); + }); + + describe('when timeDelta > 0', () => { + it('calculates for V5R1 wallet (5012 bits, 22 cells, timeDelta=54358)', () => { + // used = 5012×1 + 22×500 = 16012 + // ceil(16012 × 54358 / 2^16) = ceil(870340696 / 65536) = 13281 + expect(computeStorageFee(unitTestConfig, { bits: 5012n, cells: 22n }, 54358n)).toBe( + 13281n + ); + }); + }); +}); + +// ============================================================================ +// 3. computeForwardFee +// ============================================================================ + +describe('3. computeForwardFee', () => { + /** + * Formula: lumpPrice + ceil((bitPrice × bits + cellPrice × cells) / 2^16) + * lumpPrice = 400000, bitPrice = 26214400, cellPrice = 2621440000 + */ + + it('returns lumpPrice for bits=0, cells=0', () => { + expect(computeForwardFee(unitTestConfig, 0n, 0n)).toBe(400000n); + }); + + it('calculates for bits > 0, cells = 0', () => { + // ceil(26214400 × 667 / 2^16) + 400000 = 266800 + 400000 = 666800 + expect(computeForwardFee(unitTestConfig, 667n, 0n)).toBe(666800n); + }); + + it('calculates for bits = 0, cells > 0', () => { + // ceil(2621440000 × 1 / 2^16) + 400000 = 40000 + 400000 = 440000 + expect(computeForwardFee(unitTestConfig, 0n, 1n)).toBe(440000n); + }); + + it('calculates for bits > 0, cells > 0', () => { + // ceil((26214400×667 + 2621440000×1) / 2^16) + 400000 = 306800 + 400000 = 706800 + expect(computeForwardFee(unitTestConfig, 667n, 1n)).toBe(706800n); + }); +}); + +// ============================================================================ +// 4. computeImportFee +// ============================================================================ + +describe('4. computeImportFee', () => { + /** + * Same formula as computeForwardFee (alias). + * Used for external-in message import fee calculation. + */ + + it('uses same formula as computeForwardFee', () => { + expect(computeImportFee(unitTestConfig, 667n, 1n)).toBe( + computeForwardFee(unitTestConfig, 667n, 1n) + ); + }); +}); + +// ============================================================================ +// 5. computeActionFee +// ============================================================================ + +describe('5. computeActionFee (formula: fwdFee × firstFrac >> 16 ≈ 1/3)', () => { + it('returns 0 for fwdFee = 0', () => { + expect(computeActionFee(unitTestConfig, 0n)).toBe(0n); + }); + + it('returns ~1/3 of forward fee', () => { + // fwdFee × firstFrac >> 16 = 666672 × 21845 >> 16 = 222220 + expect(computeActionFee(unitTestConfig, 666672n)).toBe(222220n); + }); +}); + +// ============================================================================ +// 6. computeWalletGasUsed +// ============================================================================ + +describe('6. computeWalletGasUsed (formula: baseGas + gasPerMsg × n)', () => { + /** + * | Version | baseGas | gasPerMsg | + * |---------|---------|-----------| + * | V5R1 | 4222 | 717 | + * | V4R2 | 2666 | 642 | + * | V3R2 | 2352 | 642 | + * | V3R1 | 2275 | 642 | + */ + + describe('V5R1 (baseGas=4222, gasPerMsg=717)', () => { + it('1 msg: 4222 + 717×1 = 4939', () => { + expect(computeWalletGasUsed(TonWalletVersion.V5R1, 1n)).toBe(4939n); + }); + + it('3 msgs: 4222 + 717×3 = 6373', () => { + expect(computeWalletGasUsed(TonWalletVersion.V5R1, 3n)).toBe(6373n); + }); + }); + + describe('V4R2 (baseGas=2666, gasPerMsg=642)', () => { + it('1 msg: 2666 + 642×1 = 3308', () => { + expect(computeWalletGasUsed(TonWalletVersion.V4R2, 1n)).toBe(3308n); + }); + + it('3 msgs: 2666 + 642×3 = 4592', () => { + expect(computeWalletGasUsed(TonWalletVersion.V4R2, 3n)).toBe(4592n); + }); + }); + + describe('V3R2 (baseGas=2352, gasPerMsg=642)', () => { + it('1 msg: 2352 + 642×1 = 2994', () => { + expect(computeWalletGasUsed(TonWalletVersion.V3R2, 1n)).toBe(2994n); + }); + }); + + describe('V3R1 (baseGas=2275, gasPerMsg=642)', () => { + it('1 msg: 2275 + 642×1 = 2917', () => { + expect(computeWalletGasUsed(TonWalletVersion.V3R1, 1n)).toBe(2917n); + }); + }); +}); + +// ============================================================================ +// Helper functions for integration tests +// ============================================================================ + +async function loadExpected(fixture: WalletFeeTestCase): Promise { + if (shouldFetchRealFees()) { + console.log(`Fetching tx: ${fixture.txHash}`); + return fetchExpectedFees(fixture.txHash); + } + return fixture.expected; +} + +function createFeeTests(name: string, fixture: WalletFeeTestCase) { + describe(name, () => { + const { input, blockchainConfig } = fixture; + const storageUsed = input.storageUsed; + const timeDelta = input.timeDelta; + // Gas & storage use basechain config (wallet always in workchain 0) + const config = extractFeeConfig(blockchainConfig, 0); + // Convert base64 BOC to Cell + const inMsg = Cell.fromBase64(input.inMsgBoc); + // Extract outMsgs from inMsg (empty for extensions) + const outMsgs = parseWalletOutMsgCells(inMsg, input.walletVersion); + + // Extension test data (only set for extension fixtures) + const existingExtensions = input.existingExtensions; + const extensionAction = existingExtensions ? parseV5R1ExtensionAction(inMsg) : null; + const extensionHash = extensionAction?.address.hash.toString('hex') ?? ''; + const isRemoveExtension = extensionAction?.type === 'removeExtension'; + + let expected: ExpectedFees; + beforeAll(async () => { + expected = await loadExpected(fixture); + }); + + it('computeGasUsed', () => { + let gasUsed: bigint; + if (existingExtensions) { + gasUsed = isRemoveExtension + ? computeRemoveExtensionGasFromExtensions(existingExtensions, extensionHash) + : computeAddExtensionGasFromExtensions(existingExtensions, extensionHash); + } else { + gasUsed = computeWalletGasUsed(input.walletVersion, BigInt(outMsgs.length)); + } + expect(gasUsed).toBe(expected.gasUsed); + }); + + it('computeGasFee', () => { + let gasUsed: bigint; + if (existingExtensions) { + gasUsed = isRemoveExtension + ? computeRemoveExtensionGasFromExtensions(existingExtensions, extensionHash) + : computeAddExtensionGasFromExtensions(existingExtensions, extensionHash); + } else { + gasUsed = computeWalletGasUsed(input.walletVersion, BigInt(outMsgs.length)); + } + const gasFee = computeGasFee(config, gasUsed); + expect(gasFee).toBe(expected.gasFee); + }); + + it('computeActionFee', () => { + const actionFee = existingExtensions + ? 0n + : outMsgs.reduce((acc, msg) => { + const { bits, cells } = sumRefsStats(msg); + const fwdFee = computeForwardFee(config, bits, cells); + return acc + computeActionFee(config, fwdFee); + }, 0n); + expect(actionFee).toBe(expected.actionFee); + }); + + it('computeStorageFee', () => { + const storageFee = computeStorageFee(config, storageUsed, timeDelta); + expect(storageFee).toBe(expected.storageFee); + }); + + it('computeImportFee', () => { + const { bits, cells } = sumRefsStats(inMsg); + const importFee = computeImportFee(config, bits, cells); + expect(importFee).toBe(expected.importFee); + }); + + it('estimateWalletFee', () => { + const params: EstimateWalletFeeParams = existingExtensions + ? { + walletVersion: input.walletVersion as TonWalletVersion.V5R1, + storageUsed, + inMsg, + timeDelta, + existingExtensions + } + : { + walletVersion: input.walletVersion, + storageUsed, + inMsg, + timeDelta, + outMsgs + }; + const estimation = estimateWalletFee(blockchainConfig, params); + expect(estimation.gasFee).toBe(expected.gasFee); + expect(estimation.actionFee).toBe(expected.actionFee); + expect(estimation.importFee).toBe(expected.importFee); + expect(estimation.storageFee).toBe(expected.storageFee); + expect(estimation.fwdFeeRemaining).toBe(expected.fwdFeeRemaining); + expect(estimation.walletFee).toBe(expected.walletFee); + }); + }); +} + +// ============================================================================ +// 8. Blockchain-verified Transactions +// ============================================================================ + +/** + * Integration tests with real blockchain transactions. + * Each fixture contains a real transaction hash and expected fee values. + */ +describe('8. Blockchain-verified Transactions', () => { + // V3R1 - deploy + transfer (seqno=0, StateInit included) + createFeeTests('V3R1 - Deploy + Transfer', V3R1_DEPLOY_TRANSFER); + + // V3R1 - simple transfer (seqno>0, no StateInit) + createFeeTests('V3R1 - Simple TON Transfer', V3R1_SIMPLE_TRANSFER); + + // V3R1 - multi-message transfer (3 messages) + // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount + createFeeTests('V3R1 - Multi-message Transfer', V3R1_MULTI_TRANSFER); + + // V3R2 - deploy + transfer (seqno=0, StateInit included) + createFeeTests('V3R2 - Deploy + Transfer', V3R2_DEPLOY_TRANSFER); + + // V3R2 - multi-message transfer (3 messages) + // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount + createFeeTests('V3R2 - Multi-message Transfer', V3R2_MULTI_TRANSFER); + + // V3R2 - simple transfer (seqno>0, no StateInit) + createFeeTests('V3R2 - Simple TON Transfer', V3R2_SIMPLE_TRANSFER); + + // V4R2 - deploy + transfer (seqno=0, StateInit included) + createFeeTests('V4R2 - Deploy + Transfer', V4R2_DEPLOY_TRANSFER); + + // V4R2 - multi-message transfer (3 messages) + // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount + createFeeTests('V4R2 - Multi-message Transfer', V4R2_MULTI_TRANSFER); + + // V4R2 - verified against real transaction + // https://tonviewer.com/transaction/319cf6b07dd0207d48c5e4b3afe7f48228fd1fe9ff9d403987ab20c09881ceb1 + createFeeTests('V4R2 - Simple TON Transfer', V4R2_SIMPLE_TRANSFER); + + // V5R1 - deploy + transfer (seqno=0, StateInit included) + createFeeTests('V5R1 - Deploy + Transfer', V5R1_DEPLOY_TRANSFER); + + // V5R1 - verified against real transaction + // https://tonviewer.com/transaction/fea78ce4af53ea89cfaacde7359d10a43f23b4a90ce9b451516b8cddb41ba3b7 + createFeeTests('V5R1 - Simple TON Transfer', V5R1_SIMPLE_TRANSFER); + + // V5R1 - send all balance (mode 130) + // Verifies that sendMode doesn't affect gas calculation + createFeeTests('V5R1 - Send All Transfer', V5R1_SEND_ALL_TRANSFER); + + // V5R1 - multi-message transfer (3 messages) + // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount + createFeeTests('V5R1 - Multi-message Transfer', V5R1_MULTI_TRANSFER); + + // V5R1 - deploy + jetton transfer (POSASYVAET) + createFeeTests('V5R1 - Deploy + Jetton Transfer', V5R1_JETTON_DEPLOY_TRANSFER); + + // V5R1 - simple jetton transfer (USDT) + createFeeTests('V5R1 - Simple Jetton Transfer', V5R1_JETTON_SIMPLE_TRANSFER); + + // V5R1 - cell deduplication test 3.1 (duplicate refs within single message) + createFeeTests('V5R1 - Dedup Within Msg', V5R1_DEDUP_WITHIN_MSG); + + // V5R1 - cell deduplication test 3.2 (3 messages with same body) + createFeeTests('V5R1 - Dedup Cross Msg', V5R1_DEDUP_CROSS_MSG); + + // V5R1 - library cell test 3.3 (exotic cell in message body) + // CAVEAT: Verifies fee calculation counts 264 bits, but does NOT prove + // TVM recognizes it as valid library cell (never loaded/dereferenced) + createFeeTests('V5R1 - Library Cell Body', V5R1_LIBRARY_BODY); + + // V5R1 - extension test 5.1 (add first extension) + // Key difference: outMsgs=[] → actionFee=0, gasUsed differs (dict operations) + createFeeTests('V5R1 - Add First Extension', V5R1_EXTENSION_ADD_FIRST); + + // V5R1 - extension test 5.2 (add second extension) + // Tests Patricia trie insertion: cellLoads=1, gasUsed=7210 + createFeeTests('V5R1 - Add Second Extension', V5R1_EXTENSION_ADD_SECOND); + + // V5R1 - extension test 5.3 (add eighth extension) + // Tests Patricia trie with new prefix branch: pathDepth=1, subtree>1 + createFeeTests('V5R1 - Add Eighth Extension', V5R1_EXTENSION_ADD_EIGHTH); + + // V5R1 - extension test 5.4 (add ninth extension) + // Tests deep fork: 00000005 vs 00000004 differ at bit 254 + createFeeTests('V5R1 - Add Ninth Extension', V5R1_EXTENSION_ADD_NINTH); + + // V5R1 - REMOVE extension tests + // Formula: 5290 + 600×cellLoads + (siblingIsFork || rootCollapse ? 75 : 0) + createFeeTests('V5R1 - Remove Ext (LEAF sibling)', V5R1_REMOVE_EXT_LEAF_SIBLING); + createFeeTests('V5R1 - Remove Ext (FORK sibling)', V5R1_REMOVE_EXT_FORK_SIBLING); + createFeeTests('V5R1 - Remove Ext (2→1)', V5R1_REMOVE_EXT_PRELAST); + createFeeTests('V5R1 - Remove Ext (1→0)', V5R1_REMOVE_EXT_LAST); +}); + +// ============================================================================ +// 7. V5R1 Extension Gas +// ============================================================================ + +/** + * Extension gas calculation tests (blockchain-verified). + * + * ADD formula: 6610 + 600 × cellLoads (first: 6110) + * REMOVE formula: 5290 + 600 × cellLoads + (merge ? 75 : 0) + * + * Test data from wallet UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 + * All extension operations verified against real blockchain transactions. + */ +describe('7. V5R1 Extension Gas', () => { + // Extension hashes in order they were added to the wallet + const EXT_1 = '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526'; + const EXT_2 = 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522'; + const EXT_3 = '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556'; + const EXT_4 = '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e'; + const EXT_5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01'; + const EXT_6 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02'; + const EXT_7 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03'; + const EXT_8 = '0000000000000000000000000000000000000000000000000000000000000004'; + + describe('computeAddExtensionGasFromExtensions', () => { + // TX: 3eb607af0ee02aa773c9e840c817e62e2addc0871a6d6bdcd30e95784840a95e + it('0→1: empty dict → 6110', () => { + expect(computeAddExtensionGasFromExtensions([], EXT_1)).toBe(6110n); + }); + + // TX: 185a5fd6fe0a996786b7acd4b2a5ff3b69df8475be91118d4ba726d90c4bc8f3 + it('1→2: 1 ext → 7210', () => { + expect(computeAddExtensionGasFromExtensions([EXT_1], EXT_2)).toBe(7210n); + }); + + // TX: d505f6df24065a837fe0e3916b4dffdadf4de45f20b784a6862c58b7609c9828 + it('2→3: 2 ext → 7810', () => { + expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2], EXT_3)).toBe(7810n); + }); + + // TX: e89c1640cd32335a123caa6737ac3767447f8818ff911bad03a3ba3555361565 + it('3→4: 3 ext → 8410', () => { + expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3], EXT_4)).toBe(8410n); + }); + + // TX: bdfdae4d4ddd87f0e45ee2249701e01538ec4d28a711e44b7debd2ba0c680f7b + it('4→5: 4 ext → 7810', () => { + expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3, EXT_4], EXT_5)).toBe( + 7810n + ); + }); + + // TX: ca13fd5b2d0321128b265a7b4e1155ca142a08f8cc01523370385b05ab978e69 + it('5→6: 5 ext → 8410', () => { + expect( + computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3, EXT_4, EXT_5], EXT_6) + ).toBe(8410n); + }); + + // TX: 2e63cf4af8192d34f963656c632715ab66689a862d6f78e703360d3352adf07d + it('6→7: 6 ext → 9010', () => { + expect( + computeAddExtensionGasFromExtensions( + [EXT_1, EXT_2, EXT_3, EXT_4, EXT_5, EXT_6], + EXT_7 + ) + ).toBe(9010n); + }); + + // TX: a24db9110975efc27875b5786240384e96e58b29bf5497fefc84b8914f20a8a0 + it('7→8: 7 ext → 7810', () => { + expect( + computeAddExtensionGasFromExtensions( + [EXT_1, EXT_2, EXT_3, EXT_4, EXT_5, EXT_6, EXT_7], + EXT_8 + ) + ).toBe(7810n); + }); + }); + + describe('computeAddFirstExtensionGas', () => { + it('returns 6110 for empty dict → 1 extension', () => { + expect(computeAddFirstExtensionGas()).toBe(6110n); + }); + }); + + describe('computeAddExtensionGas (formula: 6610 + 600×cellLoads)', () => { + it('cellLoads=1 → 7210', () => { + expect(computeAddExtensionGas(1n)).toBe(7210n); + }); + + it('cellLoads=2 → 7810', () => { + expect(computeAddExtensionGas(2n)).toBe(7810n); + }); + + it('cellLoads=3 → 8410', () => { + expect(computeAddExtensionGas(3n)).toBe(8410n); + }); + + it('cellLoads=4 → 9010', () => { + expect(computeAddExtensionGas(4n)).toBe(9010n); + }); + }); + + // ---- REMOVE Extension ---- + + describe('computeRemoveLastExtensionGas', () => { + it('returns 5865 for 1→0 (5290 + 600 - 25)', () => { + expect(computeRemoveLastExtensionGas()).toBe(5865n); + }); + }); + + describe('computeRemoveExtensionGas (formula: 5290 + 600×cellLoads ± merge)', () => { + it('cellLoads=1, no merge → 5890', () => { + expect(computeRemoveExtensionGas(1n, false)).toBe(5890n); + }); + + it('cellLoads=1, with merge (+75) → 5965', () => { + expect(computeRemoveExtensionGas(1n, true)).toBe(5965n); + }); + + it('cellLoads=2, no merge → 6490', () => { + expect(computeRemoveExtensionGas(2n, false)).toBe(6490n); + }); + + it('cellLoads=4, no merge → 7690', () => { + expect(computeRemoveExtensionGas(4n, false)).toBe(7690n); + }); + }); + + describe('computeRemoveExtensionGasFromExtensions', () => { + it('1→0: last extension → 5865', () => { + expect(computeRemoveExtensionGasFromExtensions([EXT_1], EXT_1)).toBe(5865n); + }); + + it('2→1: root collapse (+75) → 6565', () => { + expect(computeRemoveExtensionGasFromExtensions([EXT_1, EXT_2], EXT_2)).toBe(6565n); + }); + }); +}); diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts new file mode 100644 index 000000000..1c6807ec4 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts @@ -0,0 +1,34 @@ +/** + * TON blockchain configuration snapshots for testing. + * Structure matches BlockchainConfig from @ton-api/client. + * + * Config keys: + * - 18: storage_prices + * - 20/21: gas_limits_prices (masterchain/basechain) + * - 24/25: msg_forward_prices (masterchain/basechain) + */ +import { FeeBlockchainConfig } from '../../compat'; + +export const BLOCKCHAIN_CONFIG_2024_12: FeeBlockchainConfig = { + '18': { + storagePrices: [ + { + bitPricePs: 1, + cellPricePs: 500 + } + ] + }, + '21': { + gasLimitsPrices: { + gasPrice: 26_214_400 + } + }, + '25': { + msgForwardPrices: { + lumpPrice: 400_000, + bitPrice: 26_214_400, + cellPrice: 2_621_440_000, + firstFrac: 21845 + } + } +} as const; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts new file mode 100644 index 000000000..7cdfe1e0e --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts @@ -0,0 +1,209 @@ +/** + * Helper to fetch real transaction data from TON blockchain via tonapi. + * Used in tests when FETCH_REAL_FEES=1 environment variable is set. + */ + +const TONAPI_BASE_URL = 'https://tonapi.io/v2'; + +export interface ExpectedFees { + gasUsed: bigint; + gasFee: bigint; + actionFee: bigint; + storageFee: bigint; + importFee: bigint; + fwdFeeRemaining: bigint; + walletFee: bigint; +} + +// tonapi transaction response types (partial) +interface TonApiComputePhase { + gas_fees?: number; + gas_used?: number; +} + +interface TonApiStoragePhase { + fees_collected?: number; +} + +interface TonApiActionPhase { + fwd_fees?: number; // total forward fees = actionFee + fwdFeeRemaining + total_fees?: number; // action fees only +} + +interface TonApiTransaction { + total_fees: number; + utime: number; + lt: number; + compute_phase?: TonApiComputePhase; + storage_phase?: TonApiStoragePhase; + action_phase?: TonApiActionPhase; +} + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Fetch expected fees from tonapi by transaction hash. + * Includes retry logic for rate limiting (429). + */ +export async function fetchExpectedFees(txHash: string): Promise { + const url = `${TONAPI_BASE_URL}/blockchain/transactions/${txHash}`; + const maxRetries = 3; + const baseDelay = 1000; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const response = await fetch(url); + + if (response.ok) { + return await response.json().then(parseFees); + } + + if (response.status === 429 && attempt < maxRetries) { + const delay = baseDelay * Math.pow(2, attempt); + console.log(`Rate limited, retrying in ${delay}ms...`); + await sleep(delay); + continue; + } + + throw new Error(`Failed to fetch transaction ${txHash}: ${response.status}`); + } + + throw new Error(`Failed to fetch transaction ${txHash} after ${maxRetries} retries`); +} + +function parseFees(data: TonApiTransaction): ExpectedFees { + const computePhase = data.compute_phase ?? {}; + const storagePhase = data.storage_phase ?? {}; + const actionPhase = data.action_phase ?? {}; + + const gasUsed = BigInt(computePhase.gas_used || 0); + const gasFee = BigInt(computePhase.gas_fees || 0); + const actionFee = BigInt(actionPhase.total_fees || 0); + const storageFee = BigInt(storagePhase.fees_collected || 0); + + // fwd_fees = actionFee + fwdFeeRemaining (full forward fee) + // If no action_phase (extension actions), fwdFeeRemaining = 0 + const totalFwdFees = BigInt(actionPhase.fwd_fees || 0); + const fwdFeeRemaining = totalFwdFees - actionFee; + + // Calculate importFee from total_fees + const totalFees = BigInt(data.total_fees || 0); + const importFee = totalFees - gasFee - actionFee - storageFee; + + // walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + + return { + gasUsed, + gasFee, + actionFee, + storageFee, + importFee, + fwdFeeRemaining, + walletFee + }; +} + +/** + * Check if we should fetch real data from blockchain. + * Set FETCH_REAL_FEES=1 to enable fetching. + */ +export function shouldFetchRealFees(): boolean { + return process.env.FETCH_REAL_FEES === '1'; +} + +export interface VerifyResult { + match: boolean; + actual: ExpectedFees; + diff: { + gasUsed: bigint; + gasFee: bigint; + actionFee: bigint; + storageFee: bigint; + importFee: bigint; + fwdFeeRemaining: bigint; + walletFee: bigint; + }; +} + +/** + * Verify that real transaction matches our predictions. + * Returns match status and detailed diff for debugging. + */ +export async function verifyTransactionFees( + txHash: string, + expected: ExpectedFees +): Promise { + const actual = await fetchExpectedFees(txHash); + + const diff = { + gasUsed: actual.gasUsed - expected.gasUsed, + gasFee: actual.gasFee - expected.gasFee, + actionFee: actual.actionFee - expected.actionFee, + storageFee: actual.storageFee - expected.storageFee, + importFee: actual.importFee - expected.importFee, + fwdFeeRemaining: actual.fwdFeeRemaining - expected.fwdFeeRemaining, + walletFee: actual.walletFee - expected.walletFee + }; + + const match = Object.values(diff).every(d => d === 0n); + + return { match, actual, diff }; +} + +/** + * Format verification result for console output. + */ +export function formatVerifyResult(result: VerifyResult): string { + const lines: string[] = []; + + if (result.match) { + lines.push('MATCH: All fees match predictions exactly!'); + } else { + lines.push('MISMATCH: Fees differ from predictions'); + } + + lines.push(''); + lines.push('Field | Predicted | Actual | Diff'); + lines.push('----------------|-----------|-----------|----------'); + + const format = (name: string, pred: bigint, act: bigint, diff: bigint): string => { + const diffStr = diff === 0n ? '0' : diff > 0n ? `+${diff}` : `${diff}`; + return `${name.padEnd(15)} | ${String(pred).padStart(9)} | ${String(act).padStart(9)} | ${diffStr}`; + }; + + // Reconstruct predicted from actual - diff + const predicted = { + gasUsed: result.actual.gasUsed - result.diff.gasUsed, + gasFee: result.actual.gasFee - result.diff.gasFee, + actionFee: result.actual.actionFee - result.diff.actionFee, + storageFee: result.actual.storageFee - result.diff.storageFee, + importFee: result.actual.importFee - result.diff.importFee, + fwdFeeRemaining: result.actual.fwdFeeRemaining - result.diff.fwdFeeRemaining, + walletFee: result.actual.walletFee - result.diff.walletFee + }; + + lines.push(format('gasUsed', predicted.gasUsed, result.actual.gasUsed, result.diff.gasUsed)); + lines.push(format('gasFee', predicted.gasFee, result.actual.gasFee, result.diff.gasFee)); + lines.push( + format('actionFee', predicted.actionFee, result.actual.actionFee, result.diff.actionFee) + ); + lines.push( + format('storageFee', predicted.storageFee, result.actual.storageFee, result.diff.storageFee) + ); + lines.push( + format('importFee', predicted.importFee, result.actual.importFee, result.diff.importFee) + ); + lines.push( + format( + 'fwdFeeRemaining', + predicted.fwdFeeRemaining, + result.actual.fwdFeeRemaining, + result.diff.fwdFeeRemaining + ) + ); + lines.push( + format('walletFee', predicted.walletFee, result.actual.walletFee, result.diff.walletFee) + ); + + return lines.join('\n'); +} diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts new file mode 100644 index 000000000..d0d786151 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts @@ -0,0 +1,204 @@ +import { + beginCell, + Cell, + Message, + Slice, + loadMessage, + storeMessageRelaxed, + OutActionSendMsg +} from '@ton/core'; +import { KeyPair, mnemonicToPrivateKey } from '@ton/crypto'; +import { loadOutListExtendedV5R1 } from '@ton/ton/dist/wallets/v5r1/WalletV5R1Actions'; +import { readFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +import { assertUnreachable, TonWalletVersion, FeeBlockchainConfig } from '../../compat'; +import { CellStats } from '../../fees'; + +export type WalletFeeTestCase = { + txHash: string; + input: { + inMsgBoc: string; // base64 encoded BOC + walletVersion: TonWalletVersion; + storageUsed: CellStats; + timeDelta: bigint; + existingExtensions?: string[]; // only for extension tests + }; + expected: { + gasUsed: bigint; + gasFee: bigint; + actionFee: bigint; + storageFee: bigint; + importFee: bigint; + fwdFeeRemaining: bigint; + walletFee: bigint; + }; + blockchainConfig: FeeBlockchainConfig; +}; + +export function normalizeHash(message: Message, normalizeExternal: boolean): Buffer { + if (!normalizeExternal || message.info.type !== 'external-in') { + return message.body.hash(); + } + + const cell = beginCell() + .storeUint(2, 2) // external-in + .storeUint(0, 2) // addr_none + .storeAddress(message.info.dest) + .storeUint(0, 4) // import_fee = 0 + .storeBit(false) // no StateInit + .storeBit(true) // body as reference + .storeRef(message.body) + .endCell(); + + return cell.hash(); +} + +/** + * Replace dummy signature in wallet message body. + * V3/V4: signature at start (first 512 bits) + * V5: signature at end (last 512 bits) + */ +export function replaceSignature( + dummyBody: Cell, + realSignature: Buffer, + version: TonWalletVersion +): Cell { + const slice = dummyBody.beginParse(); + const builder = beginCell(); + + if (version === TonWalletVersion.V5R1) { + // V5: signing message + signature (signature at end) + const totalBits = slice.remainingBits; + const signingBits = totalBits - 512; + builder.storeBits(slice.loadBits(signingBits)); + while (slice.remainingRefs > 0) { + builder.storeRef(slice.loadRef()); + } + builder.storeBuffer(realSignature); + } else if ( + version === TonWalletVersion.V4R2 || + version === TonWalletVersion.V3R2 || + version === TonWalletVersion.V3R1 + ) { + // V3/V4: signature + signing message (signature at start) + slice.skip(512); + builder.storeBuffer(realSignature); + builder.storeBits(slice.loadBits(slice.remainingBits)); + while (slice.remainingRefs > 0) { + builder.storeRef(slice.loadRef()); + } + } else { + assertUnreachable(version); + } + + return builder.endCell(); +} + +/** + * Parse V5R1 wallet body and extract out messages. + * Structure: opcode(32) | wallet_id(32) | timeout(32) | seqno(32) | out_list_extended | signature(512) + */ +function parseV5R1OutMsgs(bodySlice: Slice): Cell[] { + const opcode = bodySlice.loadUint(32); + + // 0x7369676e = "sign" (external signed) + // 0x73696e74 = "sint" (internal signed) + if (opcode !== 0x7369676e && opcode !== 0x73696e74) { + return []; + } + + bodySlice.loadUint(32); // wallet_id + bodySlice.loadUint(32); // timeout (valid_until) + bodySlice.loadUint(32); // seqno + + // Use @ton/ton to parse out_list_extended + const actions = loadOutListExtendedV5R1(bodySlice) as (OutActionSendMsg | { type: string })[]; + + // Filter sendMsg actions and serialize back to Cell + return actions + .filter((a): a is OutActionSendMsg => a.type === 'sendMsg') + .map(a => beginCell().store(storeMessageRelaxed(a.outMsg)).endCell()); +} + +/** + * Parse V3/V4 wallet body and extract out messages. + * V3: signature(512) | wallet_id(32) | timeout(32) | seqno(32) | [mode(8) | ^message]+ + * V4: signature(512) | wallet_id(32) | timeout(32) | seqno(32) | op(32)? | [mode(8) | ^message]+ + */ +function parseV3V4OutMsgs(bodySlice: Slice): Cell[] { + bodySlice.skip(512); // signature + bodySlice.loadUint(32); // wallet_id + bodySlice.loadUint(32); // timeout (valid_until) + bodySlice.loadUint(32); // seqno + + // Messages stored inline: [mode(8) | ^message]+ + const outMsgs: Cell[] = []; + while (bodySlice.remainingRefs > 0 && bodySlice.remainingBits >= 8) { + bodySlice.loadUint(8); // send_mode + outMsgs.push(bodySlice.loadRef()); + } + return outMsgs; +} + +/** + * Parse wallet external message and extract out messages as Cell[]. + * Uses @ton/core and @ton/ton for proper TL-B parsing. + */ +export function parseWalletOutMsgCells(inMsg: Cell, version: TonWalletVersion): Cell[] { + const message = loadMessage(inMsg.beginParse()); + const bodySlice = message.body.beginParse(); + + if (version === TonWalletVersion.V5R1) { + return parseV5R1OutMsgs(bodySlice); + } + + if ( + version === TonWalletVersion.V4R2 || + version === TonWalletVersion.V3R2 || + version === TonWalletVersion.V3R1 + ) { + return parseV3V4OutMsgs(bodySlice); + } + + assertUnreachable(version); +} + +export async function loadMnemonicKeyPair(): Promise { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const envPath = join(__dirname, '..', '.env'); + + if (!existsSync(envPath)) { + throw new Error('.env file not found. Create it with TON_MNEMONIC.'); + } + + const content = readFileSync(envPath, 'utf-8'); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const match = trimmed.match(/^TON_MNEMONIC=(.*)$/); + if (match) { + let value = match[1].trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + const mnemonic = value.split(' '); + + if (mnemonic.length !== 24) { + throw new Error('TON_MNEMONIC must be 24 words'); + } + + const keyPair = await mnemonicToPrivateKey(mnemonic); + + return keyPair; + } + } + + throw new Error('TON_MNEMONIC not found in .env'); +} diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts new file mode 100644 index 000000000..a39a43e56 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts @@ -0,0 +1,41 @@ +/** + * V3R1 Deploy + Simple TON Transfer + * https://tonviewer.com/transaction/d43bd4a7a00ee3160cd266013020a126f5662094ed8b10fd54ea7ee56c64ef2d + * + * Wallet: UQAuxK2L3BqMiY8KOTxDYvLTWS64R6gD0Xh_Ar8MTOmaIjBa + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * Deploy: YES (seqno=0, StateInit included) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; +import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; + +// === Export === + +export const V3R1_DEPLOY_TRANSFER: WalletFeeTestCase = { + txHash: '56d703d5a575c1ebebc1ca4c4d53a0fe153868f1819e90dce5454aaa60f85cbe', + + input: { + inMsgBoc: + 'te6cckECBAEAAUsAA+GIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2EZAlKodvdHYSVEbp2r8c7Ovqg9QjJz6RTyYGGJmk1EbVcqKWiZ/4OROX1EHhkLSDTpPgt4wwrrxtrfORRdd+8qBlNTRi/////+AAAAAAcAECAwDA/wAg3SCCAUyXupcw7UTQ1wsf4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBkZXBsb3kgdGVzdCOmdwU=', + walletVersion: TonWalletVersion.V3R1, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 25832n // 1765897809 - 1765871977 (~7.2h since funding) + }, + + expected: { + gasUsed: 2917n, + gasFee: 1_166_800n, + actionFee: 133_331n, + storageFee: 238n, + importFee: 1_182_400n, + fwdFeeRemaining: 266_669n, + walletFee: 2_749_438n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts new file mode 100644 index 000000000..fcd297142 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts @@ -0,0 +1,43 @@ +/** + * V3R1 Multi-message TON Transfer (3 messages) + * https://tonviewer.com/transaction/a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943 + * + * Wallet: EQDxajExFHtCu7AxEu195inKr8ZkI9WDJCzhegKvu2kme2TM + * Destinations: 3 different addresses + * Value: 0.01 TON each + * seqno: 2 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount + * Expected: gasUsed = 2275 + 642*3 = 4201 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// === Export === + +export const V3R1_MULTI_TRANSFER: WalletFeeTestCase = { + txHash: 'a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943', + + input: { + inMsgBoc: + 'te6cckECBAEAAVUAA+OIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2A1v/MHLv+Ml1GqL+s0qf7DWsmXD5MHneAK5FevAbsHo1W8COSizPAKXbac9yyyJPQFvrtyShlcz9YX89cYpGuCFNTRi7ShM/YAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IxIG11bHRpIHRlc3QgMQCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjEgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBtdWx0aSB0ZXN0IDNwpkTh', + walletVersion: TonWalletVersion.V3R1, + storageUsed: { bits: 1195n, cells: 3n }, + timeDelta: 7261n // 1765959359 (utime) - 1765952098 (last_paid) + }, + + expected: { + gasUsed: 4201n, // 2275 + 642*3 + gasFee: 1_680_400n, + actionFee: 399_993n, // 3 messages + storageFee: 299n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_091_899n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts new file mode 100644 index 000000000..a7a51e6c7 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts @@ -0,0 +1,38 @@ +/** + * V3R1 Simple TON Transfer + * https://tonviewer.com/transaction/9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3 + * + * Wallet: EQDxajExFHtCu7AxEu195inKr8ZkI9WDJCzhegKvu2kme2TM + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * seqno: 1 (NOT deploy - no StateInit) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V3R1_SIMPLE_TRANSFER: WalletFeeTestCase = { + txHash: '9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3', + + input: { + inMsgBoc: + 'te6cckEBAgEAvAAB34gB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPYBb9MXN6TWOd2BFH0MHHC8e7AbH0XaKpn2ViX8n4vM4b6FN4c0n7CPo8ajbuNmDsu8CxTI7dNwXlW2Rq6V5GEwaU1NGLtKElyAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBzaW1wbGUgdGVzdOJYMOI=', + walletVersion: TonWalletVersion.V3R1, + storageUsed: { bits: 1195n, cells: 3n }, + timeDelta: 54291n // 1765952100 (send @ 13:15) - 1765897809 (last_paid) + }, + + expected: { + gasUsed: 2917n, + gasFee: 1_166_800n, + actionFee: 133_331n, + storageFee: 2233n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_236_233n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts new file mode 100644 index 000000000..d5537b015 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts @@ -0,0 +1,39 @@ +/** + * V3R2 Deploy + Simple TON Transfer + * https://tonviewer.com/transaction/c222ab3fd903f3e14e89f571d7fc4662036150381675b31f14dc62eb7955abae + * + * Wallet: UQCscC8Yeutc-J4LJFsRsFsUKM8qerLuCx7TRUl9tjqVLYym + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * Deploy: YES (seqno=0, StateInit included) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; +import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; + +export const V3R2_DEPLOY_TRANSFER: WalletFeeTestCase = { + txHash: 'a3c4513865506e14d8eb05e0c2e508827125d615384bbb1f91b19c2147088c99', + + input: { + inMsgBoc: + 'te6cckECBAEAAVoAA+GIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaEYeccEtypc03z0Rh18v2sNOdvavY009n1UlBH0rd5oZPI7LZzgEdRGHhsFEwt5WlK8Okip/eG7g6a/GWGbNcceAFNTRi/////+AAAAAAcAECAwDe/wAg3SCCAUyXuiGCATOcurGfcbDtRNDTH9MfMdcL/+ME4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBkZXBsb3kgdGVzdKcWeY4=', + walletVersion: TonWalletVersion.V3R2, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 25861n // 1765897812 - 1765871951 (~7.2h since funding) + }, + + expected: { + gasUsed: 2994n, + gasFee: 1_197_600n, + actionFee: 133_331n, + storageFee: 238n, + importFee: 1_230_400n, + fwdFeeRemaining: 266_669n, + walletFee: 2_828_238n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts new file mode 100644 index 000000000..9c3282c67 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts @@ -0,0 +1,41 @@ +/** + * V3R2 Multi-message TON Transfer (3 messages) + * https://tonviewer.com/transaction/9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298 + * + * Wallet: EQBFk5RG37Dt-23Egx2RfJ2IHU_Kg99Wh0geO8Kf2V-VrSXr + * Destinations: 3 different addresses + * Value: 0.01 TON each + * seqno: 2 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount + * Expected: gasUsed = 2352 + 642*3 = 4278 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V3R2_MULTI_TRANSFER: WalletFeeTestCase = { + txHash: '9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298', + + input: { + inMsgBoc: + 'te6cckECBAEAAVUAA+OIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaB34zOGFqj3w0evXwQI/ANdZwbVqjHPreRk+zMd9RNGEWZydn2T1g68qTAxs5RMAAdzQn5p/x+A5v57KYgvgzkFFNTRi7ShOIgAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IyIG11bHRpIHRlc3QgMQCQYgB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPZzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjIgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBtdWx0aSB0ZXN0IDPXKVZl', + walletVersion: TonWalletVersion.V3R2, + storageUsed: { bits: 1315n, cells: 3n }, + timeDelta: 1101n // 1765960460 (utime) - 1765959359 (last_paid) + }, + + expected: { + gasUsed: 4278n, // 2352 + 642*3 + gasFee: 1_711_200n, + actionFee: 399_993n, + storageFee: 48n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_122_448n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts new file mode 100644 index 000000000..779c333db --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts @@ -0,0 +1,38 @@ +/** + * V3R2 Simple TON Transfer + * https://tonviewer.com/transaction/2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7 + * + * Wallet: EQBFk5RG37Dt-23Egx2RfJ2IHU_Kg99Wh0geO8Kf2V-VrSXr + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * seqno: 1 (NOT deploy - no StateInit) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V3R2_SIMPLE_TRANSFER: WalletFeeTestCase = { + txHash: '2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7', + + input: { + inMsgBoc: + 'te6cckEBAgEAvAAB34gAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1oGN2uEUMoi+OFETECx05z1AFr7rFZqUgxapDpSA0ZyN4et1EkPIow8h6Dwqgw/NXaa33DrEQJp9WT5aouP4q54SU1NGLtKEqPAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBzaW1wbGUgdGVzdI5CrMA=', + walletVersion: TonWalletVersion.V3R2, + storageUsed: { bits: 1315n, cells: 3n }, + timeDelta: 56575n // 1765954387 (utime) - 1765897812 (last_paid) + }, + + expected: { + gasUsed: 2994n, + gasFee: 1_197_600n, + actionFee: 133_331n, + storageFee: 2_431n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_267_231n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts new file mode 100644 index 000000000..635e9851f --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts @@ -0,0 +1,39 @@ +/** + * V4R2 Deploy + Simple TON Transfer + * https://tonviewer.com/transaction/9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6 + * + * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpkQCdBlZWYi5iV_Q6-tP + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * Deploy: YES (seqno=0, StateInit included) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; +import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; + +export const V4R2_DEPLOY_TRANSFER: WalletFeeTestCase = { + txHash: '9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6', + + input: { + inMsgBoc: + 'te6cckECFwEAA78AA+OIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWEZR9QrhoVohsiDIWH/WY1dxOdqwnAvf+v3obhBMYcdEyVVSqyAXvBIhl3JveXrQW/n7NFBiStGX16/QSgf/ZawHlNTRi/////+AAAAAAAHABFRYBFP8A9KQT9LzyyAsCAgEgAxACAUgEBwLm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQUGAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAgPAgEgCQ4CAVgKCwA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIAwNABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xESExQAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cQACOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgZGVwbG95IHRlc3QU7ERC', + walletVersion: TonWalletVersion.V4R2, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 29189n // 1765901103 - 1765871914 (real tx utime) + }, + + expected: { + gasUsed: 3308n, + gasFee: 1_323_200n, + actionFee: 133_331n, + storageFee: 269n, + importFee: 3_740_000n, + fwdFeeRemaining: 266_669n, + walletFee: 5_463_469n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts new file mode 100644 index 000000000..015f18b87 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts @@ -0,0 +1,41 @@ +/** + * V4R2 Multi-message TON Transfer (3 messages) + * https://tonviewer.com/transaction/f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d + * + * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpiQCdBmWWYi5iV_Q62Yc + * Destinations: 3 different addresses + * Value: 0.01 TON each + * seqno: 2 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount + * Expected: gasUsed = 2666 + 642*3 = 4592 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V4R2_MULTI_TRANSFER: WalletFeeTestCase = { + txHash: 'f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d', + + input: { + inMsgBoc: + 'te6cckECBAEAAVYAA+WIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWAWW792PeJs2k3RpePkQV3QSb7A76RXTzp7YyH7Hl3TE5Xl23gFlLONfJWsx/ruBNK24WPIX/mjYtHdgkvYuGSAFNTRi7ShN44AAAABAAGBgcAQIDAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjRSMiBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNFIyIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgbXVsdGkgdGVzdCAzaX8lDA==', + walletVersion: TonWalletVersion.V4R2, + storageUsed: { bits: 5689n, cells: 22n }, + timeDelta: 650n // 1765961110 (utime) - 1765960460 (last_paid) + }, + + expected: { + gasUsed: 4592n, // 2666 + 642*3 + gasFee: 1_836_800n, + actionFee: 399_993n, + storageFee: 166n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_248_166n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts new file mode 100644 index 000000000..5e9e834af --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts @@ -0,0 +1,38 @@ +/** + * V4R2 Simple TON Transfer + * https://tonviewer.com/transaction/da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11 + * + * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpiQCdBmWWYi5iV_Q62Yc + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * seqno: 1 (NOT deploy - no StateInit) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V4R2_SIMPLE_TRANSFER: WalletFeeTestCase = { + txHash: 'da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11', + + input: { + inMsgBoc: + 'te6cckEBAgEAvQAB4YgB2b0OaJ5lYx5crO5IV1omwVY/E0xIBOgzLLMRcxK/odYA/yGGDBicSwhIT4Px0xBMWKKXCRIh4qn6VaY0lmXkTTLKlvg+tIAxGf5fP04JxrwPT9EPGvHt/vwE+TsvkouoQU1NGLtKErSgAAAACAAcAQCOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgc2ltcGxlIHRlc3QPg4u7', + walletVersion: TonWalletVersion.V4R2, + storageUsed: { bits: 5689n, cells: 22n }, + timeDelta: 53817n // 1765954920 (utime) - 1765901103 (last_paid) + }, + + expected: { + gasUsed: 3308n, + gasFee: 1_323_200n, + actionFee: 133_331n, + storageFee: 13_705n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_404_105n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts new file mode 100644 index 000000000..b5263929e --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts @@ -0,0 +1,45 @@ +/** + * V5R1 Cross-message Deduplication Test (3.2) + * https://tonviewer.com/transaction/3dbbc6f071680ce7d609c4799d6fcea100161b88275a4102a4e37522f88703e3 + * + * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy + * Destinations: 3 different addresses + * Value: 0.01 TON each + * Body: Same comment cell for all messages (deduplicated) + * seqno: 3 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Test 3.2 - Validate cell deduplication ACROSS multiple outgoing messages. + * When multiple messages share the same body cell, it should be counted only once + * in importFee calculation (inMsg uses shared visited Set). + * + * Expected: importFee < v5r1-multi-transfer.importFee (1,419,200) due to deduplication + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_DEDUP_CROSS_MSG: WalletFeeTestCase = { + txHash: '139b5f7210fc0a86b54f447d0d060b1d843c1f52bf925057a7011461219a72c4', + + input: { + inMsgBoc: + 'te6cckECCAEAAVwAAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShSOAAAAABzvOnnqwdRZiru+O8FlP0SOyPOk4d6GXn7lXwnoaPTbtHOFftayjNBC100WUm8nT68UbS91TZ/W/I4vkTl8qVoXAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAIJiAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFnMS0AAAAAAAAAAAAAAAAAAAAAAAAZGVkdXAgdGVzdACCYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAGRlZHVwIHRlc3QAgmIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUcxLQAAAAAAAAAAAAAAAAAAAAAAABkZWR1cCB0ZXN0/q/S9A==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 1125n // 1765970307 (actual) - 1765969182 (last_paid) + }, + + expected: { + gasUsed: 6373n, // 4222 + 717*3 + gasFee: 2_549_200n, + actionFee: 399_993n, // Each outMsg counted separately (no cross-msg dedup for actionFee) + storageFee: 275n, + importFee: 1_352_000n, // vs 1,419,200 in multi-transfer (~5% savings from body dedup) + fwdFeeRemaining: 800_007n, + walletFee: 5_101_475n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts new file mode 100644 index 000000000..a9c092ef3 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts @@ -0,0 +1,43 @@ +/** + * V5R1 Within-message Deduplication Test (3.1) + * https://tonviewer.com/transaction/440d91a1e727fa59efbe420dc747b265a284ae4bb2d7d8b0e4919ea16e2b0965 + * + * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy + * Destination: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL + * Value: 0.01 TON + * Body: Custom cell with 2 refs to the SAME cell + * seqno: 2 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Test 3.1 - Validate cell deduplication WITHIN a single message. + * The message body has 2 references to the same cell (same hash). + * countUniqueCellStats should count it as 2 cells (body + 1 shared), not 3. + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_DEDUP_WITHIN_MSG: WalletFeeTestCase = { + txHash: '0339b0c0720456038ecfcaedc19f287341cd2df5ee0891321540070da554d054', + + input: { + inMsgBoc: + 'te6cckEBBQEA3AAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFGrAAAAAFbwDwxgL1Ime6rKH1Otm8lnAWH3J8aUH6Jw3sySXW+Lc/ZpaJ1qchT/sGJMcuaW5TIlZ1QK04gXiCtkHKo31Ih0BAgoOw8htAwIDAAACbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAEBAA8EjRWeHNoYXJlZCBkYXRhIGZvciBkZWR1cCB0ZXN0je6JOA==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 2797n // 1765969182 (actual) - 1765966385 (last_paid) + }, + + expected: { + gasUsed: 4939n, // 4222 + 717*1 + gasFee: 1_975_600n, + actionFee: 178_663n, // Body with 2 refs to same cell (deduplicated) + storageFee: 684n, + importFee: 848_000n, + fwdFeeRemaining: 357_337n, + walletFee: 3_360_284n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts new file mode 100644 index 000000000..04eb234bf --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts @@ -0,0 +1,39 @@ +/** + * V5R1 Deploy + Simple TON Transfer + * https://tonviewer.com/transaction/8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb + * + * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ysoV + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * Deploy: YES (seqno=0, StateInit included) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; +import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; + +export const V5R1_DEPLOY_TRANSFER: WalletFeeTestCase = { + txHash: '8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb', + + input: { + inMsgBoc: + 'te6cckECGQEAA3kAA+eIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUEY5tLO3P///iP////+AAAAAXRMD9rgTSL3siUO+VNqBj+rGQeQcxIl4tznudt+z+R7es1YgWsujT2Y5/87YkjeecfaLUxVnEk9b9Q4Oj8WD4RAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iMRicA12bntbv7RJrTQ2/HnQNiK1KvIt9Spp831XDSbuIAIKDsPIbQMXGAAAAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBkZXBsb3kgdGVzdB3rhBA=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 5014n // 1765901103 - 1765896089 (real tx utime) + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 47n, + importFee: 3_565_200n, + fwdFeeRemaining: 266_669n, + walletFee: 5_940_847n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts new file mode 100644 index 000000000..bba78ce3d --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts @@ -0,0 +1,63 @@ +/** + * V5R1 Add Eighth Extension + * https://tonviewer.com/transaction/c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729 + * + * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 + * Extension: 0:0000000000000000000000000000000000000000000000000000000000000004 + * seqno: 9 + * utime: 1765995008 + * + * Purpose: Test 5.3 - Validate fee estimation for adding extension + * when there are 7 existing extensions. + * + * Patricia trie analysis (7 existing extensions): + * - 613f... (0110...), 4758... (0100...), 5f70... (0101...) → share prefix "01" + * - ba6e... (1011...), ffff01-03 (1111...) → bit 0 = '1' + * + * New key 0000...04 (0000...) has bit 0 = '0', bit 1 = '0'. + * Path traversal: root → left subtree (prefix "01") + * pathDepth=1, subtree has 3 elements (>1) + * + * Gas calculation (cellLoads=2): + * gasUsed = 6610 + 600 × 2 = 7810 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 7 extensions (in order they were added) +const EXISTING_EXTENSIONS = [ + '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 + 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 + '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 + '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03' // #7 +]; + +export const V5R1_EXTENSION_ADD_EIGHTH: WalletFeeTestCase = { + txHash: 'c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQvtYAAAACUCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmd3fOQSyMWUSZO4N5SeZsrdUp2AjWLKBkD1H2kJyfIpAVO7JhYFrgxCAq1IJNy8AU5vgo+0Yz5AP+MQaG8UvgZGDSb/I=', + existingExtensions: EXISTING_EXTENSIONS, // 7 existing extensions + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6349n, cells: 34n }, + timeDelta: 14098n // utime(1765995008) - last_paid(1765980910) + }, + + expected: { + gasUsed: 7810n, // cellLoads=2: 6610 + 600 × 2 = 7810 + gasFee: 3_124_000n, + actionFee: 0n, // ZERO because no outMsgs + storageFee: 5023n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_935_823n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts new file mode 100644 index 000000000..5d48477f1 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts @@ -0,0 +1,46 @@ +/** + * V5R1 Add First Extension + * https://tonviewer.com/transaction/0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a + * + * Wallet: EQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-6q9 + * Extension: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 + * seqno: 1 + * utime: 1766032992 + * + * Purpose: Test 5.1 - Validate fee estimation for extension actions + * where there are NO outMsgs, only internal wallet dictionary operations. + * + * Key difference from transfers: + * - outMsgs: [] (empty - no outgoing messages) + * - actionFee: 0 (no forward fees to calculate) + * - gasUsed: 6110 (first extension: baseGas + overhead + CELL_WRITE) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_EXTENSION_ADD_FIRST: WalletFeeTestCase = { + txHash: '0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5RwAAAAAUCgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QvNbREC1rFe++nNUEYL7i/jFMan6sWaAcQkLcFhMQRdkdHH5FaquHUva9+ECMZuspGMjlVV45P48fwaRk8G6gDO+OSV0=', + existingExtensions: [], // First extension: empty dict + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 1734n // utime(1766032992) - last_paid(1766031258) + }, + + expected: { + gasUsed: 6110n, // First extension: baseGas(4222) + overhead(1388) + CELL_WRITE(500) + gasFee: 2_444_000n, + actionFee: 0n, // ZERO because no outMsgs + storageFee: 424n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_251_224n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts new file mode 100644 index 000000000..7056e6591 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts @@ -0,0 +1,61 @@ +/** + * V5R1 Add Ninth Extension + * https://tonviewer.com/transaction/6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331 + * + * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 + * Extension: 0:0000000000000000000000000000000000000000000000000000000000000005 + * seqno: 10 + * utime: 1766038160 + * + * Purpose: Test extension addition with 8 existing extensions. + * Adding 9th extension (00000005) which neighbors 8th (00000004). + * + * Patricia trie analysis: + * - 8 existing extensions with #8 being 00000004 (0000...0100) + * - New key 00000005 (0000...0101) differs from #8 at bit 254 + * - Path: root → 0 prefix → 00 prefix → deep fork with 00000004 + * + * Gas calculation (cellLoads=3): + * gasUsed = 6610 + 600 × 3 = 8410 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 8 extensions (in order they were added) +const EXISTING_EXTENSIONS = [ + '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 + 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 + '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 + '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 + '0000000000000000000000000000000000000000000000000000000000000004' // #8 +]; + +export const V5R1_EXTENSION_ADD_NINTH: WalletFeeTestCase = { + txHash: '6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ7IoAAAACkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqOFBZ7pjTJNQIOWc/u9QNef9HkzpTWRow+9tis9REdamv1d6G7HLupjHN3bKbz+z5OUbPirI0MZ2KSXbfF2v4HJnWqds=', + existingExtensions: EXISTING_EXTENSIONS, // 8 existing extensions + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6614n, cells: 36n }, + timeDelta: 43152n // utime(1766038160) - last_paid(1765995008) + }, + + expected: { + gasUsed: 8410n, // cellLoads=3: 6610 + 600 × 3 = 8410 + gasFee: 3_364_000n, + actionFee: 0n, // ZERO because no outMsgs + storageFee: 16_208n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 4_187_008n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts new file mode 100644 index 000000000..7585429ef --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts @@ -0,0 +1,52 @@ +/** + * V5R1 Add Second Extension + * https://tonviewer.com/transaction/30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11 + * + * Wallet: EQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-6q9 + * Extension: EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd99 + * seqno: 2 + * utime: 1766034599 + * + * Purpose: Test 5.2 - Validate fee estimation for adding extension + * when there is already 1 existing extension. + * + * With 1 existing extension, trie is ROOT → LEAF. + * Adding second extension requires loading root (1 cellLoad), + * then creating fork at point of key divergence. + * + * Gas calculation (cellLoads=1): + * gasUsed = 6610 + 600 × 1 = 7210 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing extension (from first test) +// Address: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 +const EXISTING_EXTENSION_HASH = 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca'; + +export const V5R1_EXTENSION_ADD_SECOND: WalletFeeTestCase = { + txHash: '30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5nrAAAAAkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOMgSji7NZxA4UOBTvhF0xkwL27g64NPxmjxPQtAnno9A3oGEnAO11p+ilnMrEajHbiV65pR/ZZoWlLzqcrtAILHFWRfg=', + existingExtensions: [EXISTING_EXTENSION_HASH], // 1 existing extension + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5280n, cells: 23n }, + timeDelta: 1607n // utime(1766034599) - last_paid(1766032992) + }, + + expected: { + gasUsed: 7210n, // cellLoads=1: 6610 + 600 × 1 = 7210 + gasFee: 2_884_000n, + actionFee: 0n, // ZERO because no outMsgs + storageFee: 412n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_691_212n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts new file mode 100644 index 000000000..9c417f331 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts @@ -0,0 +1,40 @@ +/** + * V5R1 Deploy + Jetton Transfer (POSASYVAET) + * https://tonviewer.com/transaction/4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab + * + * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy + * Jetton: POSASYVAET (EQBR-4-x7dik6UIHSf_IE6y2i7LdPrt3dLtoilA8sObIquW8) + * Recipient: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL + * Deploy: YES (seqno=0, StateInit included) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate fee calculation for jetton transfer with wallet deployment + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_JETTON_DEPLOY_TRANSFER: WalletFeeTestCase = { + txHash: '4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab', + + input: { + inMsgBoc: + 'te6cckECGgEAA70AA+eIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6EY5tLO3P///iP////+AAAAAUXT6I0OuBJ/Jj8s0J5LMQqDo1L3ljAh2adGyCpls7/OX2XlaRAhVWxGZN+2wThSA7EULv22lQcfiEksiBwREQXAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iKJlC4pYpKjPnRwcPVBteZ/STb08sAm+NV8fKC+V2lhkoAIKDsPIbQMXGAAAAWhiAG86P6y6gCm5XLH3xO0tXwqnOO8/zrgSt4FIXPzeWbKfIBfXhAAAAAAAAAAAAAAAAAABGQCoD4p+pe5w+sQAAAABOYloCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICvS+pMw==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 103n, cells: 1n }, + timeDelta: 1454n // 1765965604 - 1765964150 (real tx utime - last_paid) + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 236_263n, + storageFee: 14n, + importFee: 3_813_200n, + fwdFeeRemaining: 472_537n, + walletFee: 6_497_614n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts new file mode 100644 index 000000000..0670c4678 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts @@ -0,0 +1,40 @@ +/** + * V5R1 Simple Jetton Transfer (USDT) + * https://tonviewer.com/transaction/e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70 + * + * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy + * Jetton: USDT (EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs) + * Recipient: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL + * Deploy: NO (seqno=1, no StateInit) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate fee calculation for jetton transfer without wallet deployment + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_JETTON_SIMPLE_TRANSFER: WalletFeeTestCase = { + txHash: 'e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70', + + input: { + inMsgBoc: + 'te6cckECBQEAAQ4AAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShQTaAAAAAwLgww5HSimZ7XvQOb7PfbRnm5uaH32GbNFdbb+ELWLhZwkUvP/wjlKG907QVOAUwG/8fakYMT0E2B3uE2vEAwfAQIKDsPIbQMCAwAAAWhiAFC9XmrYDrIpnVpvUkWdEZgLV9Lg+7dVJpKDrftKYkjqIBfXhAAAAAAAAAAAAAAAAAABBACoD4p+pe5w+sQAAAACMBhqCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICmd9p7Q==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 781n // 1765966385 (real utime) - 1765965604 (last_paid) + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 236_263n, + storageFee: 191n, + importFee: 1_011_200n, + fwdFeeRemaining: 472_537n, + walletFee: 3_695_791n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts new file mode 100644 index 000000000..bdc7066cd --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts @@ -0,0 +1,47 @@ +/** + * V5R1 Library Cell in Message Body Test (3.3) + * https://tonviewer.com/transaction/743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3 + * + * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy + * Destination: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL + * Value: 0.01 TON + * Body: Contains library reference cell (264 bits, exotic) + * seqno: 4 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Test 3.3 - Validate that fee estimator correctly handles + * library reference cells (exotic cells) in message body. + * Library cell should be counted as 264 bits (8-bit type + 256-bit hash), + * NOT dereferenced to its full content. + * + * Library used: USDT Jetton Wallet Code + * Hash: 8f452d7a4dfd74066b682365177259ed05734435be76b5fd4bd5d8af2b7c3d68 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_LIBRARY_BODY: WalletFeeTestCase = { + txHash: '743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3', + + input: { + inMsgBoc: + 'te6cckEBBQEA3gAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFOcIAAAAJc98QMupXJWIfxDzyTVwthc5JpV/rwl0fMK9iENFr6Lk2KCeFQ4Ioplf5MoYJBn7/Zpbm1jgrauI3y6cNNdErAMBAgoOw8htAwIDAAABbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAECEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWhIvBQ+', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 2792n // utime=1765973099, last_paid=1765970307 + }, + + expected: { + gasUsed: 4939n, // 4222 + 717*1 + gasFee: 1_975_600n, + actionFee: 181_863n, // includes 264 bits from library cell + storageFee: 683n, + importFee: 857_600n, + fwdFeeRemaining: 363_737n, + walletFee: 3_379_483n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts new file mode 100644 index 000000000..683f58476 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts @@ -0,0 +1,41 @@ +/** + * V5R1 Multi-message TON Transfer (3 messages) + * https://tonviewer.com/transaction/9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a + * + * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 + * Destinations: 3 different addresses + * Value: 0.01 TON each + * seqno: 2 + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + * + * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount + * Expected: gasUsed = 4222 + 717*3 = 6373 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_MULTI_TRANSFER: WalletFeeTestCase = { + txHash: '9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a', + + input: { + inMsgBoc: + 'te6cckECCAEAAXEAAeWIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUA5tLO3P///iLShO3YAAAABUZ50fwccMDqEOvwu6DVhQAQOt4PsclSdfdktQsk5nDCob7nMpPMfCMCQeCBX297wU0F9hpGIJYOAsjb/Nx8RoTAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY1UjEgbXVsdGkgdGVzdCAzCkLbdA==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 6338n // 1765961799 (utime) - 1765955461 (last_paid) + }, + + expected: { + gasUsed: 6373n, // 4222 + 717*3 + gasFee: 2_549_200n, + actionFee: 399_993n, + storageFee: 1_549n, + importFee: 1_419_200n, + fwdFeeRemaining: 800_007n, + walletFee: 5_169_949n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts new file mode 100644 index 000000000..cf61cdb58 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts @@ -0,0 +1,67 @@ +/** + * V5R1 Remove Extension - FORK Sibling Case + * https://tonviewer.com/transaction/dd42b5e585766b58a1e4cb1d24756d1ec4351c1f5a244a44fba1d9a8ba3f2f38 + * in_msg.hash: e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b + * + * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 + * Extension to remove: 0:613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526 (#1) + * seqno: 12 (after removing #9 with seqno=11) + * + * Purpose: Test remove extension gas calculation with FORK sibling. + * + * REMOVE gas formula: + * gas = 5290 + 600×cellLoads + (needsEdgeMerge ? 75 : 0) + * where needsEdgeMerge = siblingIsFork || rootCollapse(2→1) + * + * When removing #1 (613fbe57...) from 8 extensions: + * - cellLoads = 4 (root → 0-branch → fork → leaf) + * - sibling = FORK containing #3 and #4 → siblingIsFork = true + * - needsEdgeMerge = true → +75 gas + * - gas = 5290 + 2400 + 75 = 7765 ✓ + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 8 extensions after #9 was removed (state after seqno=11) +const EXISTING_EXTENSIONS = [ + '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 - to be removed + 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 + '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 (sibling branch) + '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 (sibling branch) + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 + '0000000000000000000000000000000000000000000000000000000000000004' // #8 +]; + +export const V5R1_REMOVE_EXT_FORK_SIBLING: WalletFeeTestCase = { + txHash: 'e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ69pAAAADEDgAwn98rwtMdTA2s8ZKvxso6TvVt1A6rLGwDYxmdDzUCkx+GSZ8tz1JN01ep+1qQ/KzjTqEiT3FmNUMJnOpxT/wcablE4fzDcJLezjzb5BrdZTBYtT0H3OOqLaJiIGzR54TIGfi5Y=', + existingExtensions: EXISTING_EXTENSIONS, + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6614n, cells: 36n }, + timeDelta: 511n // utime(1766043333) - last_paid(1766042822) + }, + + expected: { + // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork ? 75 : 0) + // With 8 extensions, removing #1: + // - cellLoads = 4 (root → 0-branch → fork → leaf) + // - sibling = FORK (contains #3, #4) + // - gas = 5290 + 600×4 + 75 = 7765 ✓ + gasUsed: 7765n, + gasFee: 3_106_000n, // 7765 × 400 + actionFee: 0n, + storageFee: 192n, // ceil((6614×1 + 36×500) × 511 / 65536) + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_912_992n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts new file mode 100644 index 000000000..64598a2df --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts @@ -0,0 +1,57 @@ +/** + * V5R1 Remove Last Extension (1→0) + * https://tonviewer.com/transaction/9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4 + * + * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 + * Extension to remove: 0:e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca + * seqno: 4 + * utime: 1766051521 + * + * Purpose: Test remove extension gas when dict becomes empty (1→0). + * + * REMOVE gas formula (from TVM dict_delete + W5 contract analysis): + * gas = 5290 + 600×cellLoads - 25 (for 1→0 only) + * + * With 1 extension (single LEAF): + * - cellLoads = 1 (read the single leaf before removing) + * - Result dict = null → store_dict(null) doesn't need cell_reload for ref + * - gas = 5290 + 600 - 25 = 5865 + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 1 extension (before removal) +const EXISTING_EXTENSIONS = [ + 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca' // the last one - to be removed +]; + +export const V5R1_REMOVE_EXT_LAST: WalletFeeTestCase = { + txHash: '9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8/uAAAABEDgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5TWBewP4woqHupijNFCZ3KWoGsCJ0hfRU2OiPO5sL27iJwfZBrjOT5CdY3zpWvkiPt9QklKXYWfpvJ1eUtusdARKRdmyQ=', + existingExtensions: EXISTING_EXTENSIONS, + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5280n, cells: 23n }, + timeDelta: 2880n // utime(1766051521) - last_paid(1766048641) + }, + + expected: { + // Formula: gas = 5290 + 600×cellLoads - 25 + // cellLoads = 1 (read single leaf) + // -25: store_dict(null) doesn't need cell_reload for reference + // gas = 5290 + 600 - 25 = 5865 + gasUsed: 5865n, + gasFee: 2_346_000n, // 5865 × 400 + actionFee: 0n, + storageFee: 738n, // ceil((5280×1 + 23×500) × 2880 / 65536) + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_153_538n // 2_346_000 + 738 + 806_800 + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts new file mode 100644 index 000000000..0c699918b --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts @@ -0,0 +1,66 @@ +/** + * V5R1 Remove Extension - LEAF Sibling Case + * https://tonviewer.com/transaction/6da5c202e99d1d37bd7815c0392044822a3b6384d213a0d8e9cc4010edb6b676 + * in_msg.hash: 7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5 + * + * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 + * Extension to remove: 0:0000000000000000000000000000000000000000000000000000000000000005 (#9) + * seqno: 11 + * + * Purpose: Test remove extension gas calculation with LEAF sibling. + * + * REMOVE gas formula: + * gas = 5290 + 600×cellLoads + (needsEdgeMerge ? 75 : 0) + * where needsEdgeMerge = siblingIsFork || rootCollapse(2→1) + * + * When removing #9 (0000...05): + * - cellLoads = 4 (root → 0-branch → 00-fork → leaf) + * - sibling = LEAF #8 (0000...04) → siblingIsFork = false + * - needsEdgeMerge = false → +0 gas + * - gas = 5290 + 2400 + 0 = 7690 ✓ + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 9 extensions (before removal) +const EXISTING_EXTENSIONS = [ + '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 + 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 + '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 + '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 + '0000000000000000000000000000000000000000000000000000000000000004', // #8 (sibling - LEAF) + '0000000000000000000000000000000000000000000000000000000000000005' // #9 - to be removed +]; + +export const V5R1_REMOVE_EXT_LEAF_SIBLING: WalletFeeTestCase = { + txHash: '7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ61cAAAAC0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAojDvyCh9biYXrKKfypktoIDRtKjxbZduuSFCMIcHso1YlvnQ+dU11C+JRQvSb2J8VAfHjRjZFLmiBdVymUH3wdHKe5q0=', + existingExtensions: EXISTING_EXTENSIONS, + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6612n, cells: 36n }, + timeDelta: 4662n // utime(1766042822) - last_paid(1766038160) + }, + + expected: { + // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork ? 75 : 0) + // cellLoads = 4, siblingIsFork = false + // gas = 5290 + 2400 + 0 = 7690 + gasUsed: 7690n, + gasFee: 3_076_000n, // 7690 × 400 + actionFee: 0n, + storageFee: 1751n, // ceil((6612×1 + 36×500) × 4662 / 65536) + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_884_551n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts new file mode 100644 index 000000000..99d6ef81a --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts @@ -0,0 +1,63 @@ +/** + * V5R1 Remove Prelast Extension (2→1) + * https://tonviewer.com/transaction/4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47 + * + * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 + * Extension to remove: 0:0000000000000000000000000000000000000000000000000000000000000001 + * seqno: 3 + * utime: 1766048641 + * + * Purpose: Test remove extension gas when root fork collapses (2→1). + * + * REMOVE gas formula (from TVM dict_delete analysis): + * gas = 5290 + 600×cellLoads + (needsMerge ? 75 : 0) + * + * Where needsMerge = siblingIsFork OR rootCollapse (2→1) + * The +75 = 3 × cell_reload (3 × 25) for edge merge operations. + * + * With 2 extensions (ROOT FORK → 2 LEAFs): + * - cellLoads = 2 (root fork + target leaf) + * - siblingIsFork = false (sibling is LEAF) + * - rootCollapse = true (2→1 promotes remaining leaf to root) + * - gas = 5290 + 1200 + 75 = 6565 ✓ + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +// Existing 2 extensions (before removal) +const EXISTING_EXTENSIONS = [ + 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca', // #1 (first added) + '0000000000000000000000000000000000000000000000000000000000000001' // #2 - to be removed +]; + +export const V5R1_REMOVE_EXT_PRELAST: WalletFeeTestCase = { + txHash: '4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8mYAAAAA0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM+ox14QmGm2ktksbaghTgG/OlpdAlQgbv1KoNQhwELXWy9RHsOIlCLmoVzoqKn7ElDbj0CMk4b2t9mGH0uqvgNDTc/mk=', + existingExtensions: EXISTING_EXTENSIONS, + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5546n, cells: 25n }, + timeDelta: 14042n // utime(1766048641) - last_paid(1766034599) + }, + + expected: { + // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork || rootCollapse ? 75 : 0) + // cellLoads = 2 (root fork + target leaf) + // siblingIsFork = false, rootCollapse = true (2→1) + // +75 gas for edge merge (3 × cell_reload = 3 × 25) + // gas = 5290 + 1200 + 75 = 6565 + gasUsed: 6565n, + gasFee: 2_626_000n, // 6565 × 400 + actionFee: 0n, + storageFee: 3867n, // (5546×1 + 25×500) × 14042 / 65536 + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_436_667n // 2_626_000 + 3867 + 806_800 + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts new file mode 100644 index 000000000..514d09b0a --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts @@ -0,0 +1,41 @@ +/** + * V5R1 Send All Balance Transfer + * https://tonviewer.com/transaction/9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f + * + * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: ALL (entire balance ~0.37 TON) + * seqno: 5 + * Send mode: 130 (CARRY_ALL_REMAINING_BALANCE + IGNORE_ERRORS) + * + * Purpose: Test fee estimation for send-all mode. + * Verifies that sendMode doesn't affect gas calculation. + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_SEND_ALL_TRANSFER: WalletFeeTestCase = { + txHash: '9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f', + + input: { + inMsgBoc: + 'te6cckEBBAEAzAAB5YgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYDm0s7c///+ItKHuwwAAAALfm0UY5F646ThNTsVr5U1IpRiC2u40xbyGULBnenEi1OTCLRUcM9pCL9qzGl3Rwlm9hyB2RG+YNERLvVrRjSsg0BAgoOw8htggIDAAAAkmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0YehIAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNlbmQtYWxsIHRlc3Rw/eVR', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 3488n // utime(1766055009) - last_paid(1766051521) + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 853n, + importFee: 769_600n, + fwdFeeRemaining: 266_669n, + walletFee: 3_146_053n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts new file mode 100644 index 000000000..177dbe802 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts @@ -0,0 +1,38 @@ +/** + * V5R1 Simple TON Transfer + * https://tonviewer.com/transaction/8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056 + * + * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 + * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi + * Value: 0.01 TON + * seqno: 1 (NOT deploy - no StateInit) + * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; + +export const V5R1_SIMPLE_TRANSFER: WalletFeeTestCase = { + txHash: '8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056', + + input: { + inMsgBoc: + 'te6cckEBBAEAygAB5YgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QDm0s7c///+ItKEsWAAAAADVL1hvMF+kXoAPfh8o5Si/90ln+7aUZX1+q49NXnnIx02PxrF0pgHtgV5DmXSIUI8apByap2eB+AIbWIi4vuGBEBAgoOw8htAwIDAAAAjmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNpbXBsZSB0ZXN0WOFYaQ==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 54358n // 1765955461 (utime) - 1765901103 (last_paid) + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 13_281n, + importFee: 763_200n, + fwdFeeRemaining: 266_669n, + walletFee: 3_152_081n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 +}; diff --git a/packages/core/src/service/ton-blockchain/fee/compat.ts b/packages/core/src/service/ton-blockchain/fee/compat.ts new file mode 100644 index 000000000..781d45e5f --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/compat.ts @@ -0,0 +1,54 @@ +/** + * Compatibility layer for multiplatform fee module. + * + * The fee algorithm was ported from tonkeeper-multiplatform where it uses + * TonWalletVersion (string enum) and assertUnreachable from different paths. + * This file re-exports them so the algorithm code stays untouched. + */ + +export { assertUnreachable } from '../../../utils/types'; + +/** + * TonWalletVersion mirrors the multiplatform enum exactly. + * The web repo uses WalletVersion (numeric enum) from entries/wallet.ts, + * but this module uses the string enum for algorithm compatibility. + */ +export enum TonWalletVersion { + V3R1 = 'V3R1', + V3R2 = 'V3R2', + V4R2 = 'V4R2', + V5R1 = 'V5R1' +} + +/** + * Minimal blockchain config interface for fee calculation. + * Only the fields actually used by extractFeeConfig() are declared. + * Both @ton-api/client's BlockchainConfig and tonApiV2's BlockchainConfig + * satisfy this interface structurally. + */ +export interface FeeBlockchainConfig { + '18'?: { + storagePrices?: Array<{ + bitPricePs?: number; + cellPricePs?: number; + }>; + }; + '20'?: { gasLimitsPrices?: { gasPrice?: number } }; + '21'?: { gasLimitsPrices?: { gasPrice?: number } }; + '24'?: { + msgForwardPrices?: { + lumpPrice?: number; + bitPrice?: number; + cellPrice?: number; + firstFrac?: number; + }; + }; + '25'?: { + msgForwardPrices?: { + lumpPrice?: number; + bitPrice?: number; + cellPrice?: number; + firstFrac?: number; + }; + }; +} diff --git a/packages/core/src/service/ton-blockchain/fee/fees.ts b/packages/core/src/service/ton-blockchain/fee/fees.ts new file mode 100644 index 000000000..49fba9478 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/fees.ts @@ -0,0 +1,913 @@ +import { Address, Cell } from '@ton/core'; + +import { TonWalletVersion, FeeBlockchainConfig, assertUnreachable } from './compat'; + +// ============================================================================ +// Types +// ============================================================================ + +export type WorkchainId = -1 | 0; +export type CellStats = { bits: bigint; cells: bigint }; + +/** + * Storage stats for an uninitialized (uninit) TON account — before wallet deployment. + * Pass this as `storageUsed` when estimating fees for deploy transactions (seqno=0). + * + * An uninit account has no code/data/library, only the AccountStorage header: + * 103 bits = last_trans_lt(64) + Grams(4+32) + ExtraCurrency(1) + account_uninit(2), + * fitting in a single cell. + */ +export const UNINIT_ACCOUNT_STORAGE: CellStats = { bits: 103n, cells: 1n }; + +/** + * Complete wallet fee estimation with all components. + */ +export interface WalletFeeEstimation { + /** Gas fee for TVM execution */ + gasFee: bigint; + /** Action fee for sending outMsgs (≈1/3 of fwdFee, stays with sender) */ + actionFee: bigint; + /** Import fee for external message */ + importFee: bigint; + /** Storage fee for account state */ + storageFee: bigint; + /** + * Forward fee remaining (≈2/3 of fwdFee, deducted from outMsg value). + * For extension/plugin actions this is 0. + */ + fwdFeeRemaining: bigint; + + /** walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining */ + walletFee: bigint; +} + +export interface FeeConfigParams { + msgFwdBitPrice: bigint; // config 24/25 + msgFwdCellPrice: bigint; // config 24/25 + msgFwdLumpPrice: bigint; // config 24/25 + msgFwdFirstFrac: bigint; // config 24/25 + gasPrice: bigint; // config 20/21 + storageBitPrice: bigint; // config 18 + storageCellPrice: bigint; // config 18 +} + +// ============================================================================ +// Internal Constants +// ============================================================================ + +/** ceil(x / 2^16) */ +const shr16ceil = (x: bigint): bigint => (x + 0xffffn) >> 16n; + +/** Gas cost for first cell read (transforming Cell → Slice) */ +const TVM_CELL_READ_GAS = 100n; + +/** Gas cost for cell write (Builder → Cell) */ +const TVM_CELL_WRITE_GAS = 500n; + +/** Gas cost for cell reload (re-reading already loaded cell) */ +const TVM_CELL_RELOAD_GAS = 25n; + +/** V5R1 extension action overhead (parsing action, checking existence, etc.) */ +const V5R1_EXTENSION_OVERHEAD = 1388n; + +const ZERO_STATS: CellStats = { bits: 0n, cells: 0n }; + +/** V5R1 action tags */ +const V5R1_ACTION_ADD_EXTENSION = 0x02; +const V5R1_ACTION_REMOVE_EXTENSION = 0x03; + +export interface V5R1ExtensionAction { + type: 'addExtension' | 'removeExtension'; + address: Address; +} + +/** V4R2 plugin action (for future implementation) */ +export interface V4R2PluginAction { + type: 'installPlugin' | 'removePlugin'; + address: Address; +} + +// ============================================================================ +// Fee Config Extraction +// ============================================================================ + +// eslint-disable-next-line complexity +export function extractFeeConfig( + config: FeeBlockchainConfig, + workchain: WorkchainId = 0 +): FeeConfigParams { + const storageConfigKey = '18'; + const fwdConfigKey = workchain === -1 ? '24' : '25'; // masterchain / basechain + const gasConfigKey = workchain === -1 ? '20' : '21'; // masterchain / basechain + + const msgForwardPrices = config[fwdConfigKey]?.msgForwardPrices; + const gasPrices = config[gasConfigKey]?.gasLimitsPrices; + const storagePrices = config[storageConfigKey]?.storagePrices?.[0]; + + return { + msgFwdBitPrice: BigInt(msgForwardPrices?.bitPrice ?? 26214400), + msgFwdCellPrice: BigInt(msgForwardPrices?.cellPrice ?? 2621440000), + msgFwdLumpPrice: BigInt(msgForwardPrices?.lumpPrice ?? 400000), + msgFwdFirstFrac: BigInt(msgForwardPrices?.firstFrac ?? 21845), + gasPrice: BigInt(gasPrices?.gasPrice ?? 26214400), + storageBitPrice: BigInt(storagePrices?.bitPricePs ?? 1), + storageCellPrice: BigInt(storagePrices?.cellPricePs ?? 500) + }; +} + +// ============================================================================ +// Basic Fee Calculation Functions +// ============================================================================ + +/** fwdFee = lumpPrice + ceil((bitPrice * bits + cellPrice * cells) / 2^16) */ +export function computeForwardFee(config: FeeConfigParams, bits: bigint, cells: bigint): bigint { + return ( + config.msgFwdLumpPrice + + shr16ceil(config.msgFwdBitPrice * bits + config.msgFwdCellPrice * cells) + ); +} + +/** Import fee uses same formula as forward fee */ +export const computeImportFee = computeForwardFee; + +/** actionFee = floor(fwdFee * firstFrac / 2^16) — stays with sender, included in total_fees */ +export function computeActionFee(config: FeeConfigParams, fwdFee: bigint): bigint { + return (fwdFee * config.msgFwdFirstFrac) >> 16n; +} + +/** gasFee = floor(gasUsed * gasPrice / 2^16) */ +export function computeGasFee(config: FeeConfigParams, gasUsed: bigint): bigint { + return (gasUsed * config.gasPrice) >> 16n; +} + +/** storageFee = ceil((bits * bitPrice + cells * cellPrice) * timeDelta / 2^16) */ +export function computeStorageFee( + config: FeeConfigParams, + storageUsed: CellStats, + timeDelta: bigint +): bigint { + if (timeDelta <= 0n) return 0n; + const used = + storageUsed.bits * config.storageBitPrice + storageUsed.cells * config.storageCellPrice; + return shr16ceil(used * timeDelta); +} + +// ============================================================================ +// Wallet Gas Calculation +// ============================================================================ + +export function getWalletGasParams(version: TonWalletVersion): { + baseGas: bigint; + gasPerMsg: bigint; +} { + switch (version) { + case TonWalletVersion.V5R1: + return { baseGas: 4222n, gasPerMsg: 717n }; + case TonWalletVersion.V4R2: + return { baseGas: 2666n, gasPerMsg: 642n }; + case TonWalletVersion.V3R2: + return { baseGas: 2352n, gasPerMsg: 642n }; + case TonWalletVersion.V3R1: + return { baseGas: 2275n, gasPerMsg: 642n }; + default: + return assertUnreachable(version); + } +} + +export function computeWalletGasUsed(version: TonWalletVersion, outMsgsCount: bigint): bigint { + const { baseGas, gasPerMsg } = getWalletGasParams(version); + return baseGas + gasPerMsg * outMsgsCount; +} + +// ============================================================================ +// Cell Stats Utilities +// ============================================================================ + +function sumStats(a: CellStats, b: CellStats): CellStats { + return { bits: a.bits + b.bits, cells: a.cells + b.cells }; +} + +/** Count unique cells (TON deduplicates by hash) */ +export function countUniqueCellStats(cell: Cell, visited = new Set()): CellStats { + const hash = cell.hash().toString('hex'); + if (visited.has(hash)) return ZERO_STATS; + visited.add(hash); + + return cell.refs + .map(ref => countUniqueCellStats(ref, visited)) + .reduce(sumStats, { bits: BigInt(cell.bits.length), cells: 1n }); +} + +/** Sum stats of refs only (excludes root cell) */ +export function sumRefsStats(cell: Cell): CellStats { + const visited = new Set(); + return cell.refs.map(ref => countUniqueCellStats(ref, visited)).reduce(sumStats, ZERO_STATS); +} + +// ============================================================================ +// OutMsg Workchain Parser +// ============================================================================ + +/** + * Parse destination workchain from outMsg. + * Internal message: int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + * src:MsgAddressInt dest:MsgAddressInt ... + * + * Note: src can be addr_none (00) in pre-send messages (TVM fills it on send_raw_message) + */ +function parseOutMsgDestWorkchain(outMsg: Cell): WorkchainId { + const slice = outMsg.beginParse(); + const prefix = slice.loadUint(1); + if (prefix !== 0) { + // External message (ext_out_msg_info$11) - use basechain + return 0; + } + // int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + slice.loadBits(3); // ihr_disabled, bounce, bounced + slice.loadMaybeAddress(); // src (can be addr_none before send) + const dest = slice.loadAddress(); // dest (must be real address) + if (dest.workChain !== -1 && dest.workChain !== 0) { + throw new Error('Invalid destination workchain'); + } + + return dest.workChain; +} + +/** + * Compute action fee for outMsgs, checking destination workchain for each. + * Forward prices differ for masterchain (-1) vs basechain (0). + */ +function computeActionFeeForOutMsgs(config: FeeBlockchainConfig, outMsgs: Cell[]): bigint { + return outMsgs.reduce((acc, msg) => { + const destWorkchain = parseOutMsgDestWorkchain(msg); + + if (destWorkchain === -1) { + console.warn('Destination workchain is masterchain, not tested yet!!!'); + } + + const fwdConfig = extractFeeConfig(config, destWorkchain); + const { bits, cells } = sumRefsStats(msg); + const fwdFee = computeForwardFee(fwdConfig, bits, cells); + return acc + computeActionFee(fwdConfig, fwdFee); + }, 0n); +} + +/** + * Compute forward fee remaining for outMsgs. + * fwdFeeRemaining = fwdFee - actionFee ≈ 2/3 of forward fee. + * This amount is deducted from the outMsg value during delivery. + */ +export function computeFwdFeeRemaining(config: FeeBlockchainConfig, outMsgs: Cell[]): bigint { + return outMsgs.reduce((acc, msg) => { + const destWorkchain = parseOutMsgDestWorkchain(msg); + + if (destWorkchain === -1) { + console.warn('Destination workchain is masterchain, not tested yet!!!'); + } + + const fwdConfig = extractFeeConfig(config, destWorkchain); + const { bits, cells } = sumRefsStats(msg); + const fwdFee = computeForwardFee(fwdConfig, bits, cells); + const actionFee = computeActionFee(fwdConfig, fwdFee); + return acc + (fwdFee - actionFee); + }, 0n); +} + +// ============================================================================ +// V5R1 Message Parser +// ============================================================================ + +/** + * Parse V5R1 extension action from external message. + * Returns action type and address, or null if not an extension action. + */ +export function parseV5R1ExtensionAction(inMsg: Cell): V5R1ExtensionAction | null { + try { + const msgSlice = inMsg.beginParse(); + + // External-in message structure (TL-B): + // ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt import_fee:Grams + // init:(Maybe (Either StateInit ^StateInit)) + // body:(Either X ^X) + msgSlice.loadUint(2); // 10 = external-in + msgSlice.loadUint(2); // src: addr_none$00 + msgSlice.loadAddress(); // dest: wallet address + msgSlice.loadCoins(); // import_fee (usually 0) + + // Skip StateInit if present: Maybe (Either StateInit ^StateInit) + if (msgSlice.loadBit()) { + // Either: 0 = inline StateInit, 1 = ref + if (msgSlice.loadBit()) { + msgSlice.loadRef(); // ^StateInit + } else { + // Inline StateInit - skip all fields + // fixed_prefix_length: Maybe (## 5) + if (msgSlice.loadBit()) msgSlice.loadUint(5); + // special: Maybe TickTock (tick:Bool tock:Bool) + if (msgSlice.loadBit()) msgSlice.loadBits(2); + msgSlice.loadMaybeRef(); // code: Maybe ^Cell + msgSlice.loadMaybeRef(); // data: Maybe ^Cell + msgSlice.loadMaybeRef(); // library: Maybe ^Cell + } + } + + // Body: Either X ^X (0 = inline, 1 = ref) + const bodySlice = msgSlice.loadBit() ? msgSlice.loadRef().beginParse() : msgSlice; + + // V5R1 external signed request body structure: + // opcode(32) + wallet_id(32) + valid_until(32) + seqno(32) + actions + signature(512) + // + // Actions structure (storeOutListExtendedV5R1): + // - MaybeRef: basic out_actions (sendMsg list), null if empty + // - 1 bit: has_extended_actions + // - if has_extended: tag(8) + payload INLINE, then refs for more + // - signature at the END (512 bits) + const opcode = bodySlice.loadUint(32); + if (opcode !== 0x7369676e) { + // Not an external signed request (0x7369676e = "sign") + return null; + } + + bodySlice.loadUint(32); // wallet_id + bodySlice.loadUint(32); // valid_until + bodySlice.loadUint(32); // seqno + + // MaybeRef: basic actions (sendMsg list) + if (bodySlice.loadBit()) { + bodySlice.loadRef(); // skip out_list + } + + // has_extended_actions bit + if (!bodySlice.loadBit()) { + return null; // no extended actions + } + + // Extended action is stored INLINE: tag(8 bits) + address + const actionTag = bodySlice.loadUint(8); + + if (actionTag === V5R1_ACTION_ADD_EXTENSION) { + return { type: 'addExtension', address: bodySlice.loadAddress() }; + } else if (actionTag === V5R1_ACTION_REMOVE_EXTENSION) { + return { type: 'removeExtension', address: bodySlice.loadAddress() }; + } + + return null; + } catch { + return null; + } +} + +// ============================================================================ +// Main Wallet Fee Estimator +// ============================================================================ + +interface EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion; + inMsg: Cell; + timeDelta: bigint; + /** + * Account storage stats (bits & cells) at the moment of the transaction. + * - For deploy (seqno=0, account not yet active): use {@link UNINIT_ACCOUNT_STORAGE} + * - For active wallets: get from `account.storage_stat.used` (e.g. via liteserver or TonAPI) + */ + storageUsed: CellStats; +} + +/** Transfer estimation params */ +export interface EstimateTransferFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion; + outMsgs: Cell[]; + existingExtensions?: never; + existingPlugins?: never; +} + +/** V5R1 Extension action estimation params */ +export interface EstimateExtensionFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion.V5R1; + outMsgs?: never; + existingExtensions: string[]; + existingPlugins?: never; +} + +/** V4R2 Plugin action estimation params */ +export interface EstimatePluginFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion.V4R2; + outMsgs?: never; + existingExtensions?: never; + existingPlugins: string[]; +} + +export type EstimateWalletFeeParams = + | EstimateTransferFeeParams + | EstimateExtensionFeeParams + | EstimatePluginFeeParams; + +/** + * Estimate wallet transaction fee. + * + * For transfers: pass outMsgs array + * For V5R1 extension actions: pass existingExtensions (hex hashes from get_extensions) + * For V4R2 plugin actions: pass existingPlugins (hex hashes from get_plugins) + * + * @returns WalletFeeEstimation with all fee components including fwdFeeRemaining + */ +export function estimateWalletFee( + config: FeeBlockchainConfig, + params: EstimateWalletFeeParams +): WalletFeeEstimation { + const { walletVersion, inMsg, timeDelta, storageUsed } = params; + + // Gas & storage fees use basechain config (wallet is always in workchain 0) + const baseConfig = extractFeeConfig(config, 0); + + // Common fees + const { bits: msgBits, cells: msgCells } = sumRefsStats(inMsg); + const importFee = computeImportFee(baseConfig, msgBits, msgCells); + const storageFee = computeStorageFee(baseConfig, storageUsed, timeDelta); + + // === Transfer mode === + if ('outMsgs' in params && params.outMsgs) { + const gasUsed = computeWalletGasUsed(walletVersion, BigInt(params.outMsgs.length)); + const gasFee = computeGasFee(baseConfig, gasUsed); + + // Action fee checks destination workchain for each outMsg + const actionFee = computeActionFeeForOutMsgs(config, params.outMsgs); + + // fwdFeeRemaining = sum(fwdFee - actionFee) for all outMsgs + const fwdFeeRemaining = computeFwdFeeRemaining(config, params.outMsgs); + + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + // === V5R1 Extension mode === + if ('existingExtensions' in params && params.existingExtensions) { + const extensionAction = parseV5R1ExtensionAction(inMsg); + if (!extensionAction) { + throw new Error('Failed to parse extension action from inMsg'); + } + + if (extensionAction.type === 'addExtension') { + const newExtensionHash = extensionAction.address.hash.toString('hex'); + const gasUsed = computeAddExtensionGasFromExtensions( + params.existingExtensions, + newExtensionHash + ); + const gasFee = computeGasFee(baseConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + if (extensionAction.type === 'removeExtension') { + const removeExtensionHash = extensionAction.address.hash.toString('hex'); + const gasUsed = computeRemoveExtensionGasFromExtensions( + params.existingExtensions, + removeExtensionHash + ); + const gasFee = computeGasFee(baseConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + } + + // === V4R2 Plugin mode === + if ('existingPlugins' in params && params.existingPlugins) { + if (walletVersion !== TonWalletVersion.V4R2) { + throw new Error('Plugins are only supported for V4R2 wallets'); + } + + const pluginAction = parseV4R2PluginAction(inMsg); + if (!pluginAction) { + throw new Error('Failed to parse plugin action from inMsg'); + } + + if (pluginAction.type === 'installPlugin') { + const newPluginHash = pluginAction.address.hash.toString('hex'); + const gasUsed = computeInstallPluginGasFromPlugins( + params.existingPlugins, + newPluginHash + ); + const gasFee = computeGasFee(baseConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + if (pluginAction.type === 'removePlugin') { + const removePluginHash = pluginAction.address.hash.toString('hex'); + const gasUsed = computeRemovePluginGasFromPlugins( + params.existingPlugins, + removePluginHash + ); + const gasFee = computeGasFee(baseConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + } + + throw new Error('Invalid params: provide outMsgs, existingExtensions, or existingPlugins'); +} + +// ============================================================================ +// V5R1 Extension Gas Calculation +// ============================================================================ + +/** + * Compute gas for V5R1 AddExtension action. + * + * Based on TVM dict_set implementation (crypto/vm/dict.cpp): + * - Each cell traversed during insertion costs 100 gas (cell load) + * - Each cell created costs 500 gas (cell create) + * - For new key insertion: cellCreates = cellLoads + 2 + * + * Formula: + * gas = baseGas + overhead + cellLoads×100 + (cellLoads+2)×500 + * = 5610 + 100×L + 500×L + 1000 + * = 6610 + 600×cellLoads + * + * @param cellLoads - number of cells traversed from root to insertion point + */ +export function computeAddExtensionGas(cellLoads: bigint): bigint { + const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); + // baseGas(4222) + overhead(1388) + cellLoads×100 + (cellLoads+2)×500 + // = 5610 + 100×L + 500×L + 1000 = 6610 + 600×L + return ( + baseGas + + V5R1_EXTENSION_OVERHEAD + + 1000n + + cellLoads * (TVM_CELL_READ_GAS + TVM_CELL_WRITE_GAS) + ); +} + +/** + * Compute gas for adding first extension (empty dict → 1 extension). + * Special case: no trie traversal, just create one cell. + * Returns 6110 gas units. + */ +export function computeAddFirstExtensionGas(): bigint { + const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); + return baseGas + V5R1_EXTENSION_OVERHEAD + TVM_CELL_WRITE_GAS; +} + +// ============================================================================ +// Patricia Trie for Extension Gas Calculation +// ============================================================================ + +interface TrieNode { + type: 'leaf' | 'fork'; + key?: string; + labelLength?: number; + left?: TrieNode; + right?: TrieNode; +} + +function commonPrefixLength(a: string, b: string): number { + let i = 0; + while (i < a.length && i < b.length && a[i] === b[i]) i++; + return i; +} + +function buildTrie(keys: string[]): TrieNode | null { + if (keys.length === 0) return null; + if (keys.length === 1) return { type: 'leaf', key: keys[0] }; + + let prefix = keys[0]; + for (let i = 1; i < keys.length; i++) { + prefix = prefix.slice(0, commonPrefixLength(prefix, keys[i])); + } + + const left = keys.filter(k => k[prefix.length] === '0'); + const right = keys.filter(k => k[prefix.length] === '1'); + + return { + type: 'fork', + labelLength: prefix.length, + left: buildTrie(left) ?? undefined, + right: buildTrie(right) ?? undefined + }; +} + +function getAllKeys(node: TrieNode | undefined): string[] { + if (!node) return []; + if (node.type === 'leaf') return [node.key!]; + return [...getAllKeys(node.left), ...getAllKeys(node.right)]; +} + +/** + * Count cells traversed during dict_set from root to insertion point. + * + * Based on TVM dict_set implementation (crypto/vm/dict.cpp): + * - Each LabelParser creation = 1 cell load + * - Traverse until mismatch (pfx_len < label.l_bits) or reach leaf + * - Count EVERY cell visited, including pure forks (forks with no label bits) + * + * @see https://github.com/ton-blockchain/ton/blob/master/crypto/vm/dict.cpp + */ +function countCellLoads(node: TrieNode | undefined, key: string, pos = 0): number { + if (!node) return 0; + + // LabelParser label{std::move(dict), n}; -> 1 cell load + const loads = 1; + + if (node.type === 'leaf') { + // Leaf node - always loaded, even if key doesn't match + return loads; + } + + // Fork node - check if label matches + const nodeKeys = getAllKeys(node); + const labelBits = node.labelLength! - pos; // bits in this node's label + const nodePrefix = nodeKeys[0].slice(pos, node.labelLength); + const keySlice = key.slice(pos, node.labelLength); + + // Check common prefix length + let pfxLen = 0; + while (pfxLen < labelBits && nodePrefix[pfxLen] === keySlice[pfxLen]) pfxLen++; + + if (pfxLen < labelBits) { + // Mismatch in label - stop here (cell already loaded) + return loads; + } + + // Label matches (or pure fork with labelBits=0) - continue to child + const nextBit = key[node.labelLength!]; + const child = nextBit === '0' ? node.left : node.right; + + return loads + countCellLoads(child, key, node.labelLength! + 1); +} + +/** + * Compute gas for V5R1 AddExtension action from existing extensions. + * + * @param existingExtensionHashes - hex hashes of existing extensions (from get_extensions) + * @param newExtensionHash - hex hash of new extension address (256-bit, 64 chars) + * @returns gas used in gas units + * + * @example + * ```typescript + * const existingHashes = ['613fbe57...', 'ba6ede49...']; + * const newHash = newExtensionAddress.hash.toString('hex'); + * const gasUsed = computeAddExtensionGasFromExtensions(existingHashes, newHash); + * ``` + */ +export function computeAddExtensionGasFromExtensions( + existingExtensionHashes: string[], + newExtensionHash: string +): bigint { + if (existingExtensionHashes.length === 0) { + return computeAddFirstExtensionGas(); + } + + function hexToBinary(hex: string): string { + return hex + .split('') + .map(c => parseInt(c, 16).toString(2).padStart(4, '0')) + .join(''); + } + + const binaryKeys = existingExtensionHashes.map(hexToBinary); + const trie = buildTrie(binaryKeys); + + const newBinaryKey = hexToBinary(newExtensionHash); + const cellLoads = countCellLoads(trie ?? undefined, newBinaryKey); + + return computeAddExtensionGas(BigInt(cellLoads)); +} + +// ============================================================================ +// Remove Extension Gas Calculation +// ============================================================================ + +/** + * V5R1 remove extension overhead (smaller than ADD because no new cells created). + * + * Empirically derived from emulation: + * - ADD overhead: 1388 + 1000 (fork + leaf creation) = 2388 + * - REMOVE overhead: 1068 (no new cells, may merge existing) + */ +const V5R1_REMOVE_EXTENSION_OVERHEAD = 1068n; + +/** + * Extra gas when merging with FORK sibling (has 2 child refs). + * + * In TVM cell_builder_add_slice_bool, when sibling is FORK: + * - size_refs() = 2 → prefetch_ref called twice + * - Each prefetch_ref may add ~37.5 gas overhead + * + * The +75 gas equals 3 × cell_reload (3 × 25 = 75) for edge merge operations: + * - siblingIsFork case: load FORK sibling + prefetch its 2 child refs + * - rootCollapse (2→1) case: merge remaining leaf with former root + * + * Verified against blockchain: + * - DELETE from 9 ext (sibling = LEAF): gas = 7690 (no merge) + * - DELETE from 8 ext (sibling = FORK): gas = 7765 (+75 for FORK handling) + * - DELETE from 2 ext (root collapse): gas = 6565 (+75 for root merge) + */ +const V5R1_REMOVE_EDGE_MERGE_GAS = TVM_CELL_RELOAD_GAS * 3n; + +/** + * Compute gas for V5R1 RemoveExtension action. + * + * Based on TVM dict_delete implementation and emulation verification: + * - Same traversal cost as ADD (cell loads): cellLoads × (READ + WRITE) + * - Fewer cell creations: DELETE doesn't create fork/leaf, may merge edges + * - When sibling is FORK (has children), extra +75 gas for ref handling + * + * Formula derived from TVM gas costs (see docs.ton.org/tvm/gas): + * gas = baseGas + removeOverhead + cellLoads × (READ + WRITE) + (needsMerge ? 75 : 0) + * = 4222 + 1068 + cellLoads × 600 + (needsMerge ? 75 : 0) + * = 5290 + 600 × cellLoads + (needsMerge ? 75 : 0) + * + * The +75 gas (3 × cell_reload_gas = 3 × 25) is charged when edge merging occurs: + * - siblingIsFork: sibling has 2 children, each needs prefetch_ref (reload) + * - rootCollapse (2→1): root fork eliminated, remaining leaf promoted to root + * + * Compare to ADD formula: 6610 + 600 × cellLoads (1320 more gas) + * The difference accounts for: + * - No fork cell creation (-500 gas) + * - No leaf cell creation (-500 gas) + * - Different overhead operations (-320 gas) + * + * @param cellLoads - number of cells traversed from root to deletion point + * @param needsMergeGas - true if edge merge required (siblingIsFork OR rootCollapse) + */ +export function computeRemoveExtensionGas(cellLoads: bigint, needsMergeGas = false): bigint { + const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); + // baseGas(4222) + removeOverhead(1068) + cellLoads×(100 + 500) + mergeExtra + // = 5290 + 600×cellLoads + (needsMergeGas ? 75 : 0) + return ( + baseGas + + V5R1_REMOVE_EXTENSION_OVERHEAD + + cellLoads * (TVM_CELL_READ_GAS + TVM_CELL_WRITE_GAS) + + (needsMergeGas ? V5R1_REMOVE_EDGE_MERGE_GAS : 0n) + ); +} + +/** + * Compute gas for removing last extension (1 extension → empty dict). + * + * When removing the last extension, the result dict is null (empty). + * This saves 25 gas because store_dict(null) doesn't require a cell_reload + * for the reference, unlike store_dict(Cell) which needs to register the ref. + * + * Formula: 5290 + 600×1 - 25 = 5865 gas units. + * + * Analysis from TVM dict_lookup_delete + W5 contract: + * - dict_lookup_delete returns (value, null) for last element → no cb.finalize() + * - W5 calls store_dict(null) → writes 0-bit only, no cell reference + * - No cell_reload needed for null reference vs non-null Cell + */ +export function computeRemoveLastExtensionGas(): bigint { + // cellLoads = 1 (read single leaf) + // -25 because store_dict(null) doesn't need cell_reload for reference + return computeRemoveExtensionGas(1n) - TVM_CELL_RELOAD_GAS; +} + +/** + * Find sibling node after removing a key from trie. + * Returns { type: 'leaf' | 'fork', cellLoads } or null if key not found. + * + * The sibling is the other branch of the parent fork after deletion. + * When we delete a leaf, its parent fork merges with the sibling. + */ +function findDeleteInfo( + node: TrieNode | undefined, + key: string, + pos = 0 +): { cellLoads: number; siblingIsFork: boolean } | null { + if (!node) return null; + + // Count cell load for this node + const loads = 1; + + if (node.type === 'leaf') { + // Found the leaf to delete - but no sibling info here + // (sibling is determined by parent, which we track below) + return node.key === key ? { cellLoads: loads, siblingIsFork: false } : null; + } + + // Fork node - check if label matches + const nodeKeys = getAllKeys(node); + const labelBits = node.labelLength! - pos; + const nodePrefix = nodeKeys[0].slice(pos, node.labelLength); + const keySlice = key.slice(pos, node.labelLength); + + // Check common prefix length + let pfxLen = 0; + while (pfxLen < labelBits && nodePrefix[pfxLen] === keySlice[pfxLen]) pfxLen++; + + if (pfxLen < labelBits) { + // Mismatch - key not in trie + return null; + } + + // Label matches - continue to child + const nextBit = key[node.labelLength!]; + const child = nextBit === '0' ? node.left : node.right; + const sibling = nextBit === '0' ? node.right : node.left; + + const childResult = findDeleteInfo(child, key, node.labelLength! + 1); + if (!childResult) return null; + + // If child is the leaf being deleted, determine sibling type + if (child?.type === 'leaf' && child.key === key) { + // Sibling will be merged with parent fork + const siblingIsFork = sibling?.type === 'fork'; + return { cellLoads: loads + childResult.cellLoads, siblingIsFork }; + } + + // Propagate from deeper in the tree + return { cellLoads: loads + childResult.cellLoads, siblingIsFork: childResult.siblingIsFork }; +} + +/** + * Compute gas for V5R1 RemoveExtension action from existing extensions. + * + * The +75 gas penalty (3 × cell_reload = 3 × 25) applies when edge merging is needed: + * 1. siblingIsFork = true: sibling has 2 children requiring extra reloads + * 2. rootCollapse (2→1): root fork collapses, remaining leaf promoted to root + * + * Both cases require the same merge overhead because they both involve + * restructuring the Patricia trie with 3 additional cell reload operations. + * + * @param existingExtensionHashes - hex hashes of existing extensions (INCLUDING the one being removed) + * @param removeExtensionHash - hex hash of extension address to remove (256-bit, 64 chars) + * @returns gas used in gas units + */ +export function computeRemoveExtensionGasFromExtensions( + existingExtensionHashes: string[], + removeExtensionHash: string +): bigint { + if (existingExtensionHashes.length <= 1) { + return computeRemoveLastExtensionGas(); + } + + function hexToBinary(hex: string): string { + return hex + .split('') + .map(c => parseInt(c, 16).toString(2).padStart(4, '0')) + .join(''); + } + + const binaryKeys = existingExtensionHashes.map(hexToBinary); + const trie = buildTrie(binaryKeys); + + const removeBinaryKey = hexToBinary(removeExtensionHash); + const deleteInfo = findDeleteInfo(trie ?? undefined, removeBinaryKey); + + // Root collapse: when removing from 2-element dict, root fork is eliminated + // and remaining leaf is promoted to root. This merge costs +75 gas (3 cell reloads). + const isRootCollapse = existingExtensionHashes.length === 2; + + if (!deleteInfo) { + // Key not found - fallback to simple calculation + const cellLoads = countCellLoads(trie ?? undefined, removeBinaryKey); + return computeRemoveExtensionGas(BigInt(cellLoads), isRootCollapse); + } + + // needsMergeGas = siblingIsFork OR rootCollapse (both cause +75 gas) + const needsMergeGas = deleteInfo.siblingIsFork || isRootCollapse; + return computeRemoveExtensionGas(BigInt(deleteInfo.cellLoads), needsMergeGas); +} + +// ============================================================================ +// V4R2 Plugin Gas Calculation (Stubs) +// ============================================================================ + +/** + * Parse V4R2 plugin action from external message. + * TODO: implement parsing (opcodes: 0x2 = install, 0x3 = remove) + */ +export function parseV4R2PluginAction(_inMsg: Cell): V4R2PluginAction | null { + throw new Error('V4R2 plugin parsing not implemented'); +} + +/** + * Compute gas for installing a plugin. + * TODO: implement (similar to extensions with Patricia trie) + */ +export function computeInstallPluginGasFromPlugins( + _existingPlugins: string[], + _newPluginHash: string +): bigint { + throw new Error('V4R2 plugin install gas calculation not implemented'); +} + +/** + * Compute gas for removing a plugin. + * TODO: implement + */ +export function computeRemovePluginGasFromPlugins( + _existingPlugins: string[], + _removePluginHash: string +): bigint { + throw new Error('V4R2 plugin remove gas calculation not implemented'); +} diff --git a/packages/core/src/service/ton-blockchain/fee/index.ts b/packages/core/src/service/ton-blockchain/fee/index.ts new file mode 100644 index 000000000..f560825ff --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/index.ts @@ -0,0 +1,38 @@ +export { + UNINIT_ACCOUNT_STORAGE, + estimateWalletFee, + extractFeeConfig, + computeForwardFee, + computeImportFee, + computeActionFee, + computeGasFee, + computeStorageFee, + computeWalletGasUsed, + getWalletGasParams, + computeFwdFeeRemaining, + countUniqueCellStats, + sumRefsStats, + parseV5R1ExtensionAction, + computeAddExtensionGas, + computeAddFirstExtensionGas, + computeAddExtensionGasFromExtensions, + computeRemoveExtensionGas, + computeRemoveLastExtensionGas, + computeRemoveExtensionGasFromExtensions +} from './fees'; + +export type { + WalletFeeEstimation, + FeeConfigParams, + CellStats, + WorkchainId, + EstimateTransferFeeParams, + EstimateExtensionFeeParams, + EstimatePluginFeeParams, + EstimateWalletFeeParams, + V5R1ExtensionAction, + V4R2PluginAction +} from './fees'; + +export { TonWalletVersion } from './compat'; +export type { FeeBlockchainConfig } from './compat'; diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts new file mode 100644 index 000000000..909493290 --- /dev/null +++ b/packages/core/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + environment: 'node' + } +}); diff --git a/yarn.lock b/yarn.lock index c1aedaec9..3e4750f87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1818,6 +1818,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/aix-ppc64@npm:0.27.3" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm64@npm:0.17.19" @@ -1839,6 +1846,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm64@npm:0.27.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm@npm:0.17.19" @@ -1860,6 +1874,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm@npm:0.27.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-x64@npm:0.17.19" @@ -1881,6 +1902,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-x64@npm:0.27.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-arm64@npm:0.17.19" @@ -1902,6 +1930,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-arm64@npm:0.27.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-x64@npm:0.17.19" @@ -1923,6 +1958,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-x64@npm:0.27.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-arm64@npm:0.17.19" @@ -1944,6 +1986,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-arm64@npm:0.27.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-x64@npm:0.17.19" @@ -1965,6 +2014,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-x64@npm:0.27.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm64@npm:0.17.19" @@ -1986,6 +2042,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm64@npm:0.27.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm@npm:0.17.19" @@ -2007,6 +2070,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm@npm:0.27.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ia32@npm:0.17.19" @@ -2028,6 +2098,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ia32@npm:0.27.3" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-loong64@npm:0.17.19" @@ -2049,6 +2126,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-loong64@npm:0.27.3" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-mips64el@npm:0.17.19" @@ -2070,6 +2154,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-mips64el@npm:0.27.3" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ppc64@npm:0.17.19" @@ -2091,6 +2182,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ppc64@npm:0.27.3" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-riscv64@npm:0.17.19" @@ -2112,6 +2210,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-riscv64@npm:0.27.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-s390x@npm:0.17.19" @@ -2133,6 +2238,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-s390x@npm:0.27.3" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-x64@npm:0.17.19" @@ -2154,6 +2266,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-x64@npm:0.27.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.3": version: 0.25.3 resolution: "@esbuild/netbsd-arm64@npm:0.25.3" @@ -2161,6 +2280,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-arm64@npm:0.27.3" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/netbsd-x64@npm:0.17.19" @@ -2182,6 +2308,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-x64@npm:0.27.3" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.3": version: 0.25.3 resolution: "@esbuild/openbsd-arm64@npm:0.25.3" @@ -2189,6 +2322,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-arm64@npm:0.27.3" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/openbsd-x64@npm:0.17.19" @@ -2210,6 +2350,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-x64@npm:0.27.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openharmony-arm64@npm:0.27.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/sunos-x64@npm:0.17.19" @@ -2231,6 +2385,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/sunos-x64@npm:0.27.3" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-arm64@npm:0.17.19" @@ -2252,6 +2413,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-arm64@npm:0.27.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-ia32@npm:0.17.19" @@ -2273,6 +2441,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-ia32@npm:0.27.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-x64@npm:0.17.19" @@ -2294,6 +2469,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-x64@npm:0.27.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -2741,6 +2923,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -3459,6 +3648,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.57.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-android-arm-eabi@npm:4.9.5" @@ -3473,6 +3669,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-android-arm64@npm:4.57.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-android-arm64@npm:4.9.5" @@ -3487,6 +3690,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.57.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-darwin-arm64@npm:4.9.5" @@ -3501,6 +3711,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.57.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-darwin-x64@npm:4.9.5" @@ -3515,6 +3732,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.57.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-freebsd-x64@npm:4.40.1" @@ -3522,6 +3746,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.57.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.1" @@ -3529,6 +3760,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.57.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5" @@ -3543,6 +3781,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.57.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.40.1" @@ -3550,6 +3795,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.57.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.9.5" @@ -3564,6 +3816,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.57.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.9.5" @@ -3571,6 +3830,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-loong64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.57.1" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.57.1" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.1" @@ -3585,6 +3858,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.57.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.57.1" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.40.1" @@ -3592,6 +3879,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.57.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.9.5" @@ -3606,6 +3900,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.57.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.40.1" @@ -3613,6 +3914,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.57.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.40.1" @@ -3620,6 +3928,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.57.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.9.5" @@ -3634,6 +3949,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.57.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-linux-x64-musl@npm:4.9.5" @@ -3641,6 +3963,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-openbsd-x64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-openbsd-x64@npm:4.57.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.57.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.40.1" @@ -3648,6 +3984,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.57.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.9.5" @@ -3662,6 +4005,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.57.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.9.5" @@ -3669,6 +4019,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.57.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.40.1": version: 4.40.1 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.40.1" @@ -3676,6 +4033,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.57.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.9.5": version: 4.9.5 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.9.5" @@ -3742,6 +4106,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 + languageName: node + linkType: hard + "@stencil/core@npm:4.20.0": version: 4.20.0 resolution: "@stencil/core@npm:4.20.0" @@ -4041,6 +4412,7 @@ __metadata: tweetnacl: "npm:^1.0.3" typescript: "npm:^4.9.4" uuid: "npm:^11.1.0" + vitest: "npm:^4.0.18" zod: "npm:^3.25.36" languageName: unknown linkType: soft @@ -4515,6 +4887,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.3.5": version: 1.5.4 resolution: "@types/connect-history-api-fallback@npm:1.5.4" @@ -4612,6 +4994,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3, @types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -4646,6 +5035,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 + languageName: node + linkType: hard + "@types/eventsource@npm:^1.1.15": version: 1.1.15 resolution: "@types/eventsource@npm:1.1.15" @@ -5376,6 +5772,86 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/expect@npm:4.0.18" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + chai: "npm:^6.2.1" + tinyrainbow: "npm:^3.0.3" + checksum: 10/2115bff1bbcad460ce72032022e4dbcf8572c4b0fe07ca60f5644a8d96dd0dfa112986b5a1a5c5705f4548119b3b829c45d1de0838879211e0d6bb276b4ece73 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/mocker@npm:4.0.18" + dependencies: + "@vitest/spy": "npm:4.0.18" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/46f584a4c1180dfb513137bc8db6e2e3b53e141adfe964307297e98321652d86a3f2a52d80cda1f810205bd5fdcab789bb8b52a532e68f175ef1e20be398218d + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/pretty-format@npm:4.0.18" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10/4cafc7c9853097345bd94e8761bf47c2c04e00d366ac56d79928182787ff83c512c96f1dc2ce9b6aeed4d3a8c23ce12254da203783108d3c096bc398eed2a62d + languageName: node + linkType: hard + +"@vitest/runner@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/runner@npm:4.0.18" + dependencies: + "@vitest/utils": "npm:4.0.18" + pathe: "npm:^2.0.3" + checksum: 10/d7deebf086d7e084f449733ecea6c9c81737a18aafece318cbe7500e45debea00fa9dbf9315fd38aa88550dd5240a791b885ac71665f89b154d71a6c63da5836 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/snapshot@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10/50aa5fb7fca45c499c145cc2f20e53b8afb0990b53ff4a4e6447dd6f147437edc5316f22e2d82119e154c3cf7c59d44898e7b2faf7ba614ac1051cbe4d662a77 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/spy@npm:4.0.18" + checksum: 10/f7b1618ae13790105771dd2a8c973c63c018366fcc69b50f15ce5d12f9ac552efd3c1e6e5ae4ebdb6023d0b8d8f31fef2a0b1b77334284928db45c80c63de456 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/utils@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + tinyrainbow: "npm:^3.0.3" + checksum: 10/e8b2ad7bc35b2bc5590f9dc1d1a67644755da416b47ab7099a6f26792903fa0aacb81e6ba99f0f03858d9d3a1d76eeba65150a1a0849690a40817424e749c367 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": version: 1.11.6 resolution: "@webassemblyjs/ast@npm:1.11.6" @@ -7127,6 +7603,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -7878,6 +8361,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.1": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f + languageName: node + linkType: hard + "chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -9620,6 +10110,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.7.0": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10/b6f3e576a3fed4d82b0d0ad4bbf6b3a5ad694d2e7ce8c4a069560da3db6399381eaba703616a182b16dde50ce998af64e07dcf49f2ae48153b9e07be3f107087 + languageName: node + linkType: hard + "es-set-tostringtag@npm:^2.0.1": version: 2.0.2 resolution: "es-set-tostringtag@npm:2.0.2" @@ -9901,6 +10398,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.27.0": + version: 0.27.3 + resolution: "esbuild@npm:0.27.3" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.3" + "@esbuild/android-arm": "npm:0.27.3" + "@esbuild/android-arm64": "npm:0.27.3" + "@esbuild/android-x64": "npm:0.27.3" + "@esbuild/darwin-arm64": "npm:0.27.3" + "@esbuild/darwin-x64": "npm:0.27.3" + "@esbuild/freebsd-arm64": "npm:0.27.3" + "@esbuild/freebsd-x64": "npm:0.27.3" + "@esbuild/linux-arm": "npm:0.27.3" + "@esbuild/linux-arm64": "npm:0.27.3" + "@esbuild/linux-ia32": "npm:0.27.3" + "@esbuild/linux-loong64": "npm:0.27.3" + "@esbuild/linux-mips64el": "npm:0.27.3" + "@esbuild/linux-ppc64": "npm:0.27.3" + "@esbuild/linux-riscv64": "npm:0.27.3" + "@esbuild/linux-s390x": "npm:0.27.3" + "@esbuild/linux-x64": "npm:0.27.3" + "@esbuild/netbsd-arm64": "npm:0.27.3" + "@esbuild/netbsd-x64": "npm:0.27.3" + "@esbuild/openbsd-arm64": "npm:0.27.3" + "@esbuild/openbsd-x64": "npm:0.27.3" + "@esbuild/openharmony-arm64": "npm:0.27.3" + "@esbuild/sunos-x64": "npm:0.27.3" + "@esbuild/win32-arm64": "npm:0.27.3" + "@esbuild/win32-ia32": "npm:0.27.3" + "@esbuild/win32-x64": "npm:0.27.3" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/aa74b8d8a3ed8e2eea4d8421737b322f4d21215244e8fa2156c6402d49b5bda01343c220196f1e3f830a7ce92b54ef653c6c723a8cc2e912bb4d17b7398b51ae + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -10267,6 +10853,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -10425,6 +11020,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.2.2": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10/a5fada3d0c621649261f886e7d93e6bf80ce26d8a86e5d517e38301b8baec8450ab2cb94ba6e7a0a6bf2fc9ee55f54e1b06938ef1efa52ddcfeffbfa01acbbcc + languageName: node + linkType: hard + "expect@npm:^29.0.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -10606,6 +11208,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -13151,6 +13765,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "magic-string@npm:^0.30.3": version: 0.30.11 resolution: "magic-string@npm:0.30.11" @@ -13684,21 +14307,21 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.3, nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.8": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" bin: nanoid: bin/nanoid.cjs - checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 + checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48 languageName: node linkType: hard -"nanoid@npm:^3.3.8": - version: 3.3.11 - resolution: "nanoid@npm:3.3.11" +"nanoid@npm:^3.3.3, nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48 + checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 languageName: node linkType: hard @@ -14136,6 +14759,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.1.1": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10/bdcf9213361786688019345f3452b95a1dc73710e4b403c82a1994b98bad6abc31b26cb72a482128c5fd53ea9daf6fbb7d0e0e7b2b7e9c8be6d779deeccee07f + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -14581,6 +15211,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d + languageName: node + linkType: hard + "pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" @@ -14643,6 +15280,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 + languageName: node + linkType: hard + "pify@npm:^2.0.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -14806,6 +15450,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.6": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86 + languageName: node + linkType: hard + "postject@npm:^1.0.0-alpha.6": version: 1.0.0-alpha.6 resolution: "postject@npm:1.0.0-alpha.6" @@ -16062,6 +16717,96 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.43.0": + version: 4.57.1 + resolution: "rollup@npm:4.57.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.57.1" + "@rollup/rollup-android-arm64": "npm:4.57.1" + "@rollup/rollup-darwin-arm64": "npm:4.57.1" + "@rollup/rollup-darwin-x64": "npm:4.57.1" + "@rollup/rollup-freebsd-arm64": "npm:4.57.1" + "@rollup/rollup-freebsd-x64": "npm:4.57.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.57.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.57.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.57.1" + "@rollup/rollup-linux-loong64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-loong64-musl": "npm:4.57.1" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-ppc64-musl": "npm:4.57.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.57.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.57.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-x64-musl": "npm:4.57.1" + "@rollup/rollup-openbsd-x64": "npm:4.57.1" + "@rollup/rollup-openharmony-arm64": "npm:4.57.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.57.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.57.1" + "@rollup/rollup-win32-x64-gnu": "npm:4.57.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.57.1" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10/0451371339e593967c979e498fac4dfd0ba15fadf0dac96875940796307a00d62ab68460366a65f4872ae8edd9339e3d9501e8e5764c1f23e25e0951f75047c6 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -16460,6 +17205,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10/e93ff66c6531a079af8fb217240df01f980155b5dc408d2d7bebc398dd284e383eb318153bf8acd4db3c4fe799aa5b9a641e38b0ba3b1975700b1c89547ea4e7 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -16754,6 +17506,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10/2d4dc4e64e2db796de4a3c856d5943daccdfa3dd092e452a1ce059c81e9a9c29e0b9badba91b43ef0d5ff5c04ee62feb3bcc559a804e16faf447bac2d883aa99 + languageName: node + linkType: hard + "stacktracey@npm:^2.1.8": version: 2.1.8 resolution: "stacktracey@npm:2.1.8" @@ -16785,6 +17544,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.10.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10/19c9cda4f370b1ffae2b8b08c72167d8c3e5cfa972aaf5c6873f85d0ed2faa729407f5abb194dc33380708c00315002febb6f1e1b484736bfcf9361ad366013a + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0": version: 1.0.0 resolution: "stop-iteration-iterator@npm:1.0.0" @@ -17303,6 +18069,20 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10/cfa1e1418e91289219501703c4693c70708c91ffb7f040fd318d24aef419fb5a43e0c0160df9471499191968b2451d8da7f8087b08c3133c251c40d24aced06c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.13": version: 0.2.13 resolution: "tinyglobby@npm:0.2.13" @@ -17313,6 +18093,23 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10/169cc63c15e1378674180f3207c82c05bfa58fc79992e48792e8d97b4b759012f48e95297900ede24a81f0087cf329a0d85bb81109739eacf03c650127b3f6c1 + languageName: node + linkType: hard + "tmp-promise@npm:^3.0.2": version: 3.0.3 resolution: "tmp-promise@npm:3.0.3" @@ -18268,6 +19065,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:^6.0.0 || ^7.0.0": + version: 7.3.1 + resolution: "vite@npm:7.3.1" + dependencies: + esbuild: "npm:^0.27.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208 + languageName: node + linkType: hard + "vite@npm:^6.3.5": version: 6.3.5 resolution: "vite@npm:6.3.5" @@ -18323,6 +19175,65 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^4.0.18": + version: 4.0.18 + resolution: "vitest@npm:4.0.18" + dependencies: + "@vitest/expect": "npm:4.0.18" + "@vitest/mocker": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.0.18" + "@vitest/runner": "npm:4.0.18" + "@vitest/snapshot": "npm:4.0.18" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + es-module-lexer: "npm:^1.7.0" + expect-type: "npm:^1.2.2" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^3.10.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + vite: "npm:^6.0.0 || ^7.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.0.18 + "@vitest/browser-preview": 4.0.18 + "@vitest/browser-webdriverio": 4.0.18 + "@vitest/ui": 4.0.18 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10/6c6464ebcf3af83546862896fd1b5f10cb6607261bffce39df60033a288b8c1687ae1dd20002b6e4997a7a05303376d1eb58ce20afe63be052529a4378a8c165 + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -18701,6 +19612,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10/0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 + languageName: node + linkType: hard + "wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" From 9561647b5d6d9d913d65479ee887d775e41dfd16 Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 17:14:03 +0400 Subject: [PATCH 2/8] refactor(core): consolidate 26 fee test fixtures into single file --- .../ton-blockchain/fee/__tests__/fees.spec.ts | 158 +--- .../fee/__tests__/fixtures/test-cases.ts | 759 ++++++++++++++++++ .../fee/__tests__/fixtures/utils.ts | 15 + .../fixtures/v3r1-deploy-transfer.ts | 41 - .../__tests__/fixtures/v3r1-multi-transfer.ts | 43 - .../fixtures/v3r1-simple-transfer.ts | 38 - .../fixtures/v3r2-deploy-transfer.ts | 39 - .../__tests__/fixtures/v3r2-multi-transfer.ts | 41 - .../fixtures/v3r2-simple-transfer.ts | 38 - .../fixtures/v4r2-deploy-transfer.ts | 39 - .../__tests__/fixtures/v4r2-multi-transfer.ts | 41 - .../fixtures/v4r2-simple-transfer.ts | 38 - .../fixtures/v5r1-dedup-cross-msg.ts | 45 -- .../fixtures/v5r1-dedup-within-msg.ts | 43 - .../fixtures/v5r1-deploy-transfer.ts | 39 - .../fixtures/v5r1-extension-add-eighth.ts | 63 -- .../fixtures/v5r1-extension-add-first.ts | 46 -- .../fixtures/v5r1-extension-add-ninth.ts | 61 -- .../fixtures/v5r1-extension-add-second.ts | 52 -- .../fixtures/v5r1-jetton-deploy-transfer.ts | 40 - .../fixtures/v5r1-jetton-simple-transfer.ts | 40 - .../__tests__/fixtures/v5r1-library-body.ts | 47 -- .../__tests__/fixtures/v5r1-multi-transfer.ts | 41 - .../fixtures/v5r1-remove-ext-fork-sibling.ts | 67 -- .../fixtures/v5r1-remove-ext-last.ts | 57 -- .../fixtures/v5r1-remove-ext-leaf-sibling.ts | 66 -- .../fixtures/v5r1-remove-ext-prelast.ts | 63 -- .../fixtures/v5r1-send-all-transfer.ts | 41 - .../fixtures/v5r1-simple-transfer.ts | 38 - 29 files changed, 797 insertions(+), 1342 deletions(-) create mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts delete mode 100644 packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts index cf5da0d7b..16924d6cd 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -3,33 +3,8 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { BLOCKCHAIN_CONFIG_2024_12 } from './fixtures/blockchain-config'; import { fetchExpectedFees, shouldFetchRealFees, ExpectedFees } from './fixtures/tonapi-fetcher'; +import { FEE_TEST_CASES, EXT } from './fixtures/test-cases'; import { WalletFeeTestCase, parseWalletOutMsgCells } from './fixtures/utils'; -import { V3R1_DEPLOY_TRANSFER } from './fixtures/v3r1-deploy-transfer'; -import { V3R1_MULTI_TRANSFER } from './fixtures/v3r1-multi-transfer'; -import { V3R1_SIMPLE_TRANSFER } from './fixtures/v3r1-simple-transfer'; -import { V3R2_DEPLOY_TRANSFER } from './fixtures/v3r2-deploy-transfer'; -import { V3R2_MULTI_TRANSFER } from './fixtures/v3r2-multi-transfer'; -import { V3R2_SIMPLE_TRANSFER } from './fixtures/v3r2-simple-transfer'; -import { V4R2_DEPLOY_TRANSFER } from './fixtures/v4r2-deploy-transfer'; -import { V4R2_MULTI_TRANSFER } from './fixtures/v4r2-multi-transfer'; -import { V4R2_SIMPLE_TRANSFER } from './fixtures/v4r2-simple-transfer'; -import { V5R1_DEDUP_CROSS_MSG } from './fixtures/v5r1-dedup-cross-msg'; -import { V5R1_DEDUP_WITHIN_MSG } from './fixtures/v5r1-dedup-within-msg'; -import { V5R1_DEPLOY_TRANSFER } from './fixtures/v5r1-deploy-transfer'; -import { V5R1_EXTENSION_ADD_EIGHTH } from './fixtures/v5r1-extension-add-eighth'; -import { V5R1_EXTENSION_ADD_FIRST } from './fixtures/v5r1-extension-add-first'; -import { V5R1_EXTENSION_ADD_NINTH } from './fixtures/v5r1-extension-add-ninth'; -import { V5R1_EXTENSION_ADD_SECOND } from './fixtures/v5r1-extension-add-second'; -import { V5R1_JETTON_DEPLOY_TRANSFER } from './fixtures/v5r1-jetton-deploy-transfer'; -import { V5R1_JETTON_SIMPLE_TRANSFER } from './fixtures/v5r1-jetton-simple-transfer'; -import { V5R1_LIBRARY_BODY } from './fixtures/v5r1-library-body'; -import { V5R1_MULTI_TRANSFER } from './fixtures/v5r1-multi-transfer'; -import { V5R1_REMOVE_EXT_FORK_SIBLING } from './fixtures/v5r1-remove-ext-fork-sibling'; -import { V5R1_REMOVE_EXT_LAST } from './fixtures/v5r1-remove-ext-last'; -import { V5R1_REMOVE_EXT_LEAF_SIBLING } from './fixtures/v5r1-remove-ext-leaf-sibling'; -import { V5R1_REMOVE_EXT_PRELAST } from './fixtures/v5r1-remove-ext-prelast'; -import { V5R1_SEND_ALL_TRANSFER } from './fixtures/v5r1-send-all-transfer'; -import { V5R1_SIMPLE_TRANSFER } from './fixtures/v5r1-simple-transfer'; import { computeActionFee, computeAddExtensionGas, @@ -338,91 +313,9 @@ function createFeeTests(name: string, fixture: WalletFeeTestCase) { * Each fixture contains a real transaction hash and expected fee values. */ describe('8. Blockchain-verified Transactions', () => { - // V3R1 - deploy + transfer (seqno=0, StateInit included) - createFeeTests('V3R1 - Deploy + Transfer', V3R1_DEPLOY_TRANSFER); - - // V3R1 - simple transfer (seqno>0, no StateInit) - createFeeTests('V3R1 - Simple TON Transfer', V3R1_SIMPLE_TRANSFER); - - // V3R1 - multi-message transfer (3 messages) - // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount - createFeeTests('V3R1 - Multi-message Transfer', V3R1_MULTI_TRANSFER); - - // V3R2 - deploy + transfer (seqno=0, StateInit included) - createFeeTests('V3R2 - Deploy + Transfer', V3R2_DEPLOY_TRANSFER); - - // V3R2 - multi-message transfer (3 messages) - // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount - createFeeTests('V3R2 - Multi-message Transfer', V3R2_MULTI_TRANSFER); - - // V3R2 - simple transfer (seqno>0, no StateInit) - createFeeTests('V3R2 - Simple TON Transfer', V3R2_SIMPLE_TRANSFER); - - // V4R2 - deploy + transfer (seqno=0, StateInit included) - createFeeTests('V4R2 - Deploy + Transfer', V4R2_DEPLOY_TRANSFER); - - // V4R2 - multi-message transfer (3 messages) - // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount - createFeeTests('V4R2 - Multi-message Transfer', V4R2_MULTI_TRANSFER); - - // V4R2 - verified against real transaction - // https://tonviewer.com/transaction/319cf6b07dd0207d48c5e4b3afe7f48228fd1fe9ff9d403987ab20c09881ceb1 - createFeeTests('V4R2 - Simple TON Transfer', V4R2_SIMPLE_TRANSFER); - - // V5R1 - deploy + transfer (seqno=0, StateInit included) - createFeeTests('V5R1 - Deploy + Transfer', V5R1_DEPLOY_TRANSFER); - - // V5R1 - verified against real transaction - // https://tonviewer.com/transaction/fea78ce4af53ea89cfaacde7359d10a43f23b4a90ce9b451516b8cddb41ba3b7 - createFeeTests('V5R1 - Simple TON Transfer', V5R1_SIMPLE_TRANSFER); - - // V5R1 - send all balance (mode 130) - // Verifies that sendMode doesn't affect gas calculation - createFeeTests('V5R1 - Send All Transfer', V5R1_SEND_ALL_TRANSFER); - - // V5R1 - multi-message transfer (3 messages) - // Validates gas formula: gasUsed = baseGas + gasPerMsg * outMsgsCount - createFeeTests('V5R1 - Multi-message Transfer', V5R1_MULTI_TRANSFER); - - // V5R1 - deploy + jetton transfer (POSASYVAET) - createFeeTests('V5R1 - Deploy + Jetton Transfer', V5R1_JETTON_DEPLOY_TRANSFER); - - // V5R1 - simple jetton transfer (USDT) - createFeeTests('V5R1 - Simple Jetton Transfer', V5R1_JETTON_SIMPLE_TRANSFER); - - // V5R1 - cell deduplication test 3.1 (duplicate refs within single message) - createFeeTests('V5R1 - Dedup Within Msg', V5R1_DEDUP_WITHIN_MSG); - - // V5R1 - cell deduplication test 3.2 (3 messages with same body) - createFeeTests('V5R1 - Dedup Cross Msg', V5R1_DEDUP_CROSS_MSG); - - // V5R1 - library cell test 3.3 (exotic cell in message body) - // CAVEAT: Verifies fee calculation counts 264 bits, but does NOT prove - // TVM recognizes it as valid library cell (never loaded/dereferenced) - createFeeTests('V5R1 - Library Cell Body', V5R1_LIBRARY_BODY); - - // V5R1 - extension test 5.1 (add first extension) - // Key difference: outMsgs=[] → actionFee=0, gasUsed differs (dict operations) - createFeeTests('V5R1 - Add First Extension', V5R1_EXTENSION_ADD_FIRST); - - // V5R1 - extension test 5.2 (add second extension) - // Tests Patricia trie insertion: cellLoads=1, gasUsed=7210 - createFeeTests('V5R1 - Add Second Extension', V5R1_EXTENSION_ADD_SECOND); - - // V5R1 - extension test 5.3 (add eighth extension) - // Tests Patricia trie with new prefix branch: pathDepth=1, subtree>1 - createFeeTests('V5R1 - Add Eighth Extension', V5R1_EXTENSION_ADD_EIGHTH); - - // V5R1 - extension test 5.4 (add ninth extension) - // Tests deep fork: 00000005 vs 00000004 differ at bit 254 - createFeeTests('V5R1 - Add Ninth Extension', V5R1_EXTENSION_ADD_NINTH); - - // V5R1 - REMOVE extension tests - // Formula: 5290 + 600×cellLoads + (siblingIsFork || rootCollapse ? 75 : 0) - createFeeTests('V5R1 - Remove Ext (LEAF sibling)', V5R1_REMOVE_EXT_LEAF_SIBLING); - createFeeTests('V5R1 - Remove Ext (FORK sibling)', V5R1_REMOVE_EXT_FORK_SIBLING); - createFeeTests('V5R1 - Remove Ext (2→1)', V5R1_REMOVE_EXT_PRELAST); - createFeeTests('V5R1 - Remove Ext (1→0)', V5R1_REMOVE_EXT_LAST); + for (const fixture of FEE_TEST_CASES) { + createFeeTests(fixture.name, fixture); + } }); // ============================================================================ @@ -439,48 +332,43 @@ describe('8. Blockchain-verified Transactions', () => { * All extension operations verified against real blockchain transactions. */ describe('7. V5R1 Extension Gas', () => { - // Extension hashes in order they were added to the wallet - const EXT_1 = '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526'; - const EXT_2 = 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522'; - const EXT_3 = '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556'; - const EXT_4 = '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e'; - const EXT_5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01'; - const EXT_6 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02'; - const EXT_7 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03'; - const EXT_8 = '0000000000000000000000000000000000000000000000000000000000000004'; - describe('computeAddExtensionGasFromExtensions', () => { // TX: 3eb607af0ee02aa773c9e840c817e62e2addc0871a6d6bdcd30e95784840a95e it('0→1: empty dict → 6110', () => { - expect(computeAddExtensionGasFromExtensions([], EXT_1)).toBe(6110n); + expect(computeAddExtensionGasFromExtensions([], EXT.E1)).toBe(6110n); }); // TX: 185a5fd6fe0a996786b7acd4b2a5ff3b69df8475be91118d4ba726d90c4bc8f3 it('1→2: 1 ext → 7210', () => { - expect(computeAddExtensionGasFromExtensions([EXT_1], EXT_2)).toBe(7210n); + expect(computeAddExtensionGasFromExtensions([EXT.E1], EXT.E2)).toBe(7210n); }); // TX: d505f6df24065a837fe0e3916b4dffdadf4de45f20b784a6862c58b7609c9828 it('2→3: 2 ext → 7810', () => { - expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2], EXT_3)).toBe(7810n); + expect(computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2], EXT.E3)).toBe(7810n); }); // TX: e89c1640cd32335a123caa6737ac3767447f8818ff911bad03a3ba3555361565 it('3→4: 3 ext → 8410', () => { - expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3], EXT_4)).toBe(8410n); + expect(computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2, EXT.E3], EXT.E4)).toBe( + 8410n + ); }); // TX: bdfdae4d4ddd87f0e45ee2249701e01538ec4d28a711e44b7debd2ba0c680f7b it('4→5: 4 ext → 7810', () => { - expect(computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3, EXT_4], EXT_5)).toBe( - 7810n - ); + expect( + computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2, EXT.E3, EXT.E4], EXT.E5) + ).toBe(7810n); }); // TX: ca13fd5b2d0321128b265a7b4e1155ca142a08f8cc01523370385b05ab978e69 it('5→6: 5 ext → 8410', () => { expect( - computeAddExtensionGasFromExtensions([EXT_1, EXT_2, EXT_3, EXT_4, EXT_5], EXT_6) + computeAddExtensionGasFromExtensions( + [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5], + EXT.E6 + ) ).toBe(8410n); }); @@ -488,8 +376,8 @@ describe('7. V5R1 Extension Gas', () => { it('6→7: 6 ext → 9010', () => { expect( computeAddExtensionGasFromExtensions( - [EXT_1, EXT_2, EXT_3, EXT_4, EXT_5, EXT_6], - EXT_7 + [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6], + EXT.E7 ) ).toBe(9010n); }); @@ -498,8 +386,8 @@ describe('7. V5R1 Extension Gas', () => { it('7→8: 7 ext → 7810', () => { expect( computeAddExtensionGasFromExtensions( - [EXT_1, EXT_2, EXT_3, EXT_4, EXT_5, EXT_6, EXT_7], - EXT_8 + [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7], + EXT.E8 ) ).toBe(7810n); }); @@ -557,11 +445,11 @@ describe('7. V5R1 Extension Gas', () => { describe('computeRemoveExtensionGasFromExtensions', () => { it('1→0: last extension → 5865', () => { - expect(computeRemoveExtensionGasFromExtensions([EXT_1], EXT_1)).toBe(5865n); + expect(computeRemoveExtensionGasFromExtensions([EXT.E1], EXT.E1)).toBe(5865n); }); it('2→1: root collapse (+75) → 6565', () => { - expect(computeRemoveExtensionGasFromExtensions([EXT_1, EXT_2], EXT_2)).toBe(6565n); + expect(computeRemoveExtensionGasFromExtensions([EXT.E1, EXT.E2], EXT.E2)).toBe(6565n); }); }); }); diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts new file mode 100644 index 000000000..610af59de --- /dev/null +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts @@ -0,0 +1,759 @@ +/** + * Consolidated TON fee calculation test fixtures. + * + * All test cases are real blockchain transactions verified against TonAPI. + * Generated by: npx tsx generate-test-cases.ts + * + * To add a new test case: + * 1. Create a fixture file, add to generate-test-cases.ts FIXTURES array + * 2. Run: npx tsx generate-test-cases.ts + * 3. Verify: yarn workspace @tonkeeper/core exec vitest run fees.spec.ts + */ + +import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; +import { WalletFeeTestCase } from './utils'; +import { TonWalletVersion } from '../../compat'; +import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; + +// Extension hashes shared across fixtures (deduplication of 8+ fixture files) +export const EXT = { + E1: '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', + E2: 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', + E3: '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', + E4: '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', + E5: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', + E6: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', + E7: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', + E8: '0000000000000000000000000000000000000000000000000000000000000004', + E9: '0000000000000000000000000000000000000000000000000000000000000005', + FIRST: 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca', + SECOND: '0000000000000000000000000000000000000000000000000000000000000001' +} as const; + +export const FEE_TEST_CASES: WalletFeeTestCase[] = [ + // ============================================================ + // V3R1 + // ============================================================ + + { + name: 'V3R1 - Simple TON Transfer', + tag: 'simple-transfer', + txHash: '9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3', + + input: { + inMsgBoc: + 'te6cckEBAgEAvAAB34gB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPYBb9MXN6TWOd2BFH0MHHC8e7AbH0XaKpn2ViX8n4vM4b6FN4c0n7CPo8ajbuNmDsu8CxTI7dNwXlW2Rq6V5GEwaU1NGLtKElyAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBzaW1wbGUgdGVzdOJYMOI=', + walletVersion: TonWalletVersion.V3R1, + storageUsed: { bits: 1195n, cells: 3n }, + timeDelta: 54_291n + }, + + expected: { + gasUsed: 2917n, + gasFee: 1_166_800n, + actionFee: 133_331n, + storageFee: 2233n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_236_233n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V3R1 - Deploy + Transfer', + tag: 'deploy-transfer', + txHash: '56d703d5a575c1ebebc1ca4c4d53a0fe153868f1819e90dce5454aaa60f85cbe', + + input: { + inMsgBoc: + 'te6cckECBAEAAUsAA+GIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2EZAlKodvdHYSVEbp2r8c7Ovqg9QjJz6RTyYGGJmk1EbVcqKWiZ/4OROX1EHhkLSDTpPgt4wwrrxtrfORRdd+8qBlNTRi/////+AAAAAAcAECAwDA/wAg3SCCAUyXupcw7UTQ1wsf4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBkZXBsb3kgdGVzdCOmdwU=', + walletVersion: TonWalletVersion.V3R1, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 25_832n + }, + + expected: { + gasUsed: 2917n, + gasFee: 1_166_800n, + actionFee: 133_331n, + storageFee: 238n, + importFee: 1_182_400n, + fwdFeeRemaining: 266_669n, + walletFee: 2_749_438n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V3R1 - Multi-message Transfer', + tag: 'multi-transfer', + txHash: 'a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943', + + input: { + inMsgBoc: + 'te6cckECBAEAAVUAA+OIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2A1v/MHLv+Ml1GqL+s0qf7DWsmXD5MHneAK5FevAbsHo1W8COSizPAKXbac9yyyJPQFvrtyShlcz9YX89cYpGuCFNTRi7ShM/YAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IxIG11bHRpIHRlc3QgMQCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjEgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBtdWx0aSB0ZXN0IDNwpkTh', + walletVersion: TonWalletVersion.V3R1, + storageUsed: { bits: 1195n, cells: 3n }, + timeDelta: 7261n + }, + + expected: { + gasUsed: 4201n, + gasFee: 1_680_400n, + actionFee: 399_993n, + storageFee: 299n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_091_899n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V3R2 + // ============================================================ + + { + name: 'V3R2 - Simple TON Transfer', + tag: 'simple-transfer', + txHash: '2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7', + + input: { + inMsgBoc: + 'te6cckEBAgEAvAAB34gAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1oGN2uEUMoi+OFETECx05z1AFr7rFZqUgxapDpSA0ZyN4et1EkPIow8h6Dwqgw/NXaa33DrEQJp9WT5aouP4q54SU1NGLtKEqPAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBzaW1wbGUgdGVzdI5CrMA=', + walletVersion: TonWalletVersion.V3R2, + storageUsed: { bits: 1315n, cells: 3n }, + timeDelta: 56_575n + }, + + expected: { + gasUsed: 2994n, + gasFee: 1_197_600n, + actionFee: 133_331n, + storageFee: 2431n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_267_231n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V3R2 - Deploy + Transfer', + tag: 'deploy-transfer', + txHash: 'a3c4513865506e14d8eb05e0c2e508827125d615384bbb1f91b19c2147088c99', + + input: { + inMsgBoc: + 'te6cckECBAEAAVoAA+GIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaEYeccEtypc03z0Rh18v2sNOdvavY009n1UlBH0rd5oZPI7LZzgEdRGHhsFEwt5WlK8Okip/eG7g6a/GWGbNcceAFNTRi/////+AAAAAAcAECAwDe/wAg3SCCAUyXuiGCATOcurGfcbDtRNDTH9MfMdcL/+ME4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBkZXBsb3kgdGVzdKcWeY4=', + walletVersion: TonWalletVersion.V3R2, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 25_861n + }, + + expected: { + gasUsed: 2994n, + gasFee: 1_197_600n, + actionFee: 133_331n, + storageFee: 238n, + importFee: 1_230_400n, + fwdFeeRemaining: 266_669n, + walletFee: 2_828_238n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V3R2 - Multi-message Transfer', + tag: 'multi-transfer', + txHash: '9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298', + + input: { + inMsgBoc: + 'te6cckECBAEAAVUAA+OIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaB34zOGFqj3w0evXwQI/ANdZwbVqjHPreRk+zMd9RNGEWZydn2T1g68qTAxs5RMAAdzQn5p/x+A5v57KYgvgzkFFNTRi7ShOIgAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IyIG11bHRpIHRlc3QgMQCQYgB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPZzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjIgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBtdWx0aSB0ZXN0IDPXKVZl', + walletVersion: TonWalletVersion.V3R2, + storageUsed: { bits: 1315n, cells: 3n }, + timeDelta: 1101n + }, + + expected: { + gasUsed: 4278n, + gasFee: 1_711_200n, + actionFee: 399_993n, + storageFee: 48n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_122_448n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V4R2 + // ============================================================ + + { + name: 'V4R2 - Simple TON Transfer', + tag: 'simple-transfer', + txHash: 'da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11', + + input: { + inMsgBoc: + 'te6cckEBAgEAvQAB4YgB2b0OaJ5lYx5crO5IV1omwVY/E0xIBOgzLLMRcxK/odYA/yGGDBicSwhIT4Px0xBMWKKXCRIh4qn6VaY0lmXkTTLKlvg+tIAxGf5fP04JxrwPT9EPGvHt/vwE+TsvkouoQU1NGLtKErSgAAAACAAcAQCOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgc2ltcGxlIHRlc3QPg4u7', + walletVersion: TonWalletVersion.V4R2, + storageUsed: { bits: 5689n, cells: 22n }, + timeDelta: 53_817n + }, + + expected: { + gasUsed: 3308n, + gasFee: 1_323_200n, + actionFee: 133_331n, + storageFee: 13_705n, + importFee: 667_200n, + fwdFeeRemaining: 266_669n, + walletFee: 2_404_105n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V4R2 - Deploy + Transfer', + tag: 'deploy-transfer', + txHash: '9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6', + + input: { + inMsgBoc: + 'te6cckECFwEAA78AA+OIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWEZR9QrhoVohsiDIWH/WY1dxOdqwnAvf+v3obhBMYcdEyVVSqyAXvBIhl3JveXrQW/n7NFBiStGX16/QSgf/ZawHlNTRi/////+AAAAAAAHABFRYBFP8A9KQT9LzyyAsCAgEgAxACAUgEBwLm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQUGAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAgPAgEgCQ4CAVgKCwA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIAwNABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xESExQAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cQACOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgZGVwbG95IHRlc3QU7ERC', + walletVersion: TonWalletVersion.V4R2, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 29_189n + }, + + expected: { + gasUsed: 3308n, + gasFee: 1_323_200n, + actionFee: 133_331n, + storageFee: 269n, + importFee: 3_740_000n, + fwdFeeRemaining: 266_669n, + walletFee: 5_463_469n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V4R2 - Multi-message Transfer', + tag: 'multi-transfer', + txHash: 'f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d', + + input: { + inMsgBoc: + 'te6cckECBAEAAVYAA+WIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWAWW792PeJs2k3RpePkQV3QSb7A76RXTzp7YyH7Hl3TE5Xl23gFlLONfJWsx/ruBNK24WPIX/mjYtHdgkvYuGSAFNTRi7ShN44AAAABAAGBgcAQIDAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjRSMiBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNFIyIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgbXVsdGkgdGVzdCAzaX8lDA==', + walletVersion: TonWalletVersion.V4R2, + storageUsed: { bits: 5689n, cells: 22n }, + timeDelta: 650n + }, + + expected: { + gasUsed: 4592n, + gasFee: 1_836_800n, + actionFee: 399_993n, + storageFee: 166n, + importFee: 1_211_200n, + fwdFeeRemaining: 800_007n, + walletFee: 4_248_166n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V5R1 + // ============================================================ + + { + name: 'V5R1 - Simple TON Transfer', + tag: 'simple-transfer', + txHash: '8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056', + + input: { + inMsgBoc: + 'te6cckEBBAEAygAB5YgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QDm0s7c///+ItKEsWAAAAADVL1hvMF+kXoAPfh8o5Si/90ln+7aUZX1+q49NXnnIx02PxrF0pgHtgV5DmXSIUI8apByap2eB+AIbWIi4vuGBEBAgoOw8htAwIDAAAAjmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNpbXBsZSB0ZXN0WOFYaQ==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 54_358n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 13_281n, + importFee: 763_200n, + fwdFeeRemaining: 266_669n, + walletFee: 3_152_081n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Deploy + Transfer', + tag: 'deploy-transfer', + txHash: '8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb', + + input: { + inMsgBoc: + 'te6cckECGQEAA3kAA+eIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUEY5tLO3P///iP////+AAAAAXRMD9rgTSL3siUO+VNqBj+rGQeQcxIl4tznudt+z+R7es1YgWsujT2Y5/87YkjeecfaLUxVnEk9b9Q4Oj8WD4RAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iMRicA12bntbv7RJrTQ2/HnQNiK1KvIt9Spp831XDSbuIAIKDsPIbQMXGAAAAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBkZXBsb3kgdGVzdB3rhBA=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 5014n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 47n, + importFee: 3_565_200n, + fwdFeeRemaining: 266_669n, + walletFee: 5_940_847n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Multi-message Transfer', + tag: 'multi-transfer', + txHash: '9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a', + + input: { + inMsgBoc: + 'te6cckECCAEAAXEAAeWIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUA5tLO3P///iLShO3YAAAABUZ50fwccMDqEOvwu6DVhQAQOt4PsclSdfdktQsk5nDCob7nMpPMfCMCQeCBX297wU0F9hpGIJYOAsjb/Nx8RoTAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY1UjEgbXVsdGkgdGVzdCAzCkLbdA==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 6338n + }, + + expected: { + gasUsed: 6373n, + gasFee: 2_549_200n, + actionFee: 399_993n, + storageFee: 1549n, + importFee: 1_419_200n, + fwdFeeRemaining: 800_007n, + walletFee: 5_169_949n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Send All Transfer', + tag: 'send-all', + txHash: '9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f', + + input: { + inMsgBoc: + 'te6cckEBBAEAzAAB5YgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYDm0s7c///+ItKHuwwAAAALfm0UY5F646ThNTsVr5U1IpRiC2u40xbyGULBnenEi1OTCLRUcM9pCL9qzGl3Rwlm9hyB2RG+YNERLvVrRjSsg0BAgoOw8htggIDAAAAkmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0YehIAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNlbmQtYWxsIHRlc3Rw/eVR', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 3488n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 133_331n, + storageFee: 853n, + importFee: 769_600n, + fwdFeeRemaining: 266_669n, + walletFee: 3_146_053n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V5R1 Jetton + // ============================================================ + + { + name: 'V5R1 - Deploy + Jetton Transfer', + tag: 'jetton-deploy-transfer', + txHash: '4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab', + + input: { + inMsgBoc: + 'te6cckECGgEAA70AA+eIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6EY5tLO3P///iP////+AAAAAUXT6I0OuBJ/Jj8s0J5LMQqDo1L3ljAh2adGyCpls7/OX2XlaRAhVWxGZN+2wThSA7EULv22lQcfiEksiBwREQXAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iKJlC4pYpKjPnRwcPVBteZ/STb08sAm+NV8fKC+V2lhkoAIKDsPIbQMXGAAAAWhiAG86P6y6gCm5XLH3xO0tXwqnOO8/zrgSt4FIXPzeWbKfIBfXhAAAAAAAAAAAAAAAAAABGQCoD4p+pe5w+sQAAAABOYloCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICvS+pMw==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: UNINIT_ACCOUNT_STORAGE, + timeDelta: 1454n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 236_263n, + storageFee: 14n, + importFee: 3_813_200n, + fwdFeeRemaining: 472_537n, + walletFee: 6_497_614n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Simple Jetton Transfer', + tag: 'jetton-transfer', + txHash: 'e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70', + + input: { + inMsgBoc: + 'te6cckECBQEAAQ4AAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShQTaAAAAAwLgww5HSimZ7XvQOb7PfbRnm5uaH32GbNFdbb+ELWLhZwkUvP/wjlKG907QVOAUwG/8fakYMT0E2B3uE2vEAwfAQIKDsPIbQMCAwAAAWhiAFC9XmrYDrIpnVpvUkWdEZgLV9Lg+7dVJpKDrftKYkjqIBfXhAAAAAAAAAAAAAAAAAABBACoD4p+pe5w+sQAAAACMBhqCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICmd9p7Q==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 781n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 236_263n, + storageFee: 191n, + importFee: 1_011_200n, + fwdFeeRemaining: 472_537n, + walletFee: 3_695_791n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V5R1 Dedup & Exotic + // ============================================================ + + { + name: 'V5R1 - Dedup Within Msg', + tag: 'dedup-within-msg', + txHash: '0339b0c0720456038ecfcaedc19f287341cd2df5ee0891321540070da554d054', + + input: { + inMsgBoc: + 'te6cckEBBQEA3AAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFGrAAAAAFbwDwxgL1Ime6rKH1Otm8lnAWH3J8aUH6Jw3sySXW+Lc/ZpaJ1qchT/sGJMcuaW5TIlZ1QK04gXiCtkHKo31Ih0BAgoOw8htAwIDAAACbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAEBAA8EjRWeHNoYXJlZCBkYXRhIGZvciBkZWR1cCB0ZXN0je6JOA==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 2797n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 178_663n, + storageFee: 684n, + importFee: 848_000n, + fwdFeeRemaining: 357_337n, + walletFee: 3_360_284n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Dedup Cross Msg', + tag: 'dedup-cross-msg', + txHash: '139b5f7210fc0a86b54f447d0d060b1d843c1f52bf925057a7011461219a72c4', + + input: { + inMsgBoc: + 'te6cckECCAEAAVwAAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShSOAAAAABzvOnnqwdRZiru+O8FlP0SOyPOk4d6GXn7lXwnoaPTbtHOFftayjNBC100WUm8nT68UbS91TZ/W/I4vkTl8qVoXAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAIJiAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFnMS0AAAAAAAAAAAAAAAAAAAAAAAAZGVkdXAgdGVzdACCYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAGRlZHVwIHRlc3QAgmIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUcxLQAAAAAAAAAAAAAAAAAAAAAAABkZWR1cCB0ZXN0/q/S9A==', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 1125n + }, + + expected: { + gasUsed: 6373n, + gasFee: 2_549_200n, + actionFee: 399_993n, + storageFee: 275n, + importFee: 1_352_000n, + fwdFeeRemaining: 800_007n, + walletFee: 5_101_475n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Library Cell Body', + tag: 'library-body', + txHash: '743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3', + + input: { + inMsgBoc: + 'te6cckEBBQEA3gAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFOcIAAAAJc98QMupXJWIfxDzyTVwthc5JpV/rwl0fMK9iENFr6Lk2KCeFQ4Ioplf5MoYJBn7/Zpbm1jgrauI3y6cNNdErAMBAgoOw8htAwIDAAABbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAECEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWhIvBQ+', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 2792n + }, + + expected: { + gasUsed: 4939n, + gasFee: 1_975_600n, + actionFee: 181_863n, + storageFee: 683n, + importFee: 857_600n, + fwdFeeRemaining: 363_737n, + walletFee: 3_379_483n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V5R1 Add Extension + // ============================================================ + + { + name: 'V5R1 - Add First Extension', + tag: 'add-extension', + txHash: '0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5RwAAAAAUCgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QvNbREC1rFe++nNUEYL7i/jFMan6sWaAcQkLcFhMQRdkdHH5FaquHUva9+ECMZuspGMjlVV45P48fwaRk8G6gDO+OSV0=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5012n, cells: 22n }, + timeDelta: 1734n, + existingExtensions: [] + }, + + expected: { + gasUsed: 6110n, + gasFee: 2_444_000n, + actionFee: 0n, + storageFee: 424n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_251_224n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Add Second Extension', + tag: 'add-extension', + txHash: '30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5nrAAAAAkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOMgSji7NZxA4UOBTvhF0xkwL27g64NPxmjxPQtAnno9A3oGEnAO11p+ilnMrEajHbiV65pR/ZZoWlLzqcrtAILHFWRfg=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5280n, cells: 23n }, + timeDelta: 1607n, + existingExtensions: [EXT.FIRST] + }, + + expected: { + gasUsed: 7210n, + gasFee: 2_884_000n, + actionFee: 0n, + storageFee: 412n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_691_212n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Add Eighth Extension', + tag: 'add-extension', + txHash: 'c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQvtYAAAACUCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmd3fOQSyMWUSZO4N5SeZsrdUp2AjWLKBkD1H2kJyfIpAVO7JhYFrgxCAq1IJNy8AU5vgo+0Yz5AP+MQaG8UvgZGDSb/I=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6349n, cells: 34n }, + timeDelta: 14_098n, + existingExtensions: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7] + }, + + expected: { + gasUsed: 7810n, + gasFee: 3_124_000n, + actionFee: 0n, + storageFee: 5023n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_935_823n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Add Ninth Extension', + tag: 'add-extension', + txHash: '6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ7IoAAAACkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqOFBZ7pjTJNQIOWc/u9QNef9HkzpTWRow+9tis9REdamv1d6G7HLupjHN3bKbz+z5OUbPirI0MZ2KSXbfF2v4HJnWqds=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6614n, cells: 36n }, + timeDelta: 43_152n, + existingExtensions: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7, EXT.E8] + }, + + expected: { + gasUsed: 8410n, + gasFee: 3_364_000n, + actionFee: 0n, + storageFee: 16_208n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 4_187_008n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + // ============================================================ + // V5R1 Remove Extension + // ============================================================ + + { + name: 'V5R1 - Remove Ext (LEAF sibling)', + tag: 'remove-extension', + txHash: '7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ61cAAAAC0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAojDvyCh9biYXrKKfypktoIDRtKjxbZduuSFCMIcHso1YlvnQ+dU11C+JRQvSb2J8VAfHjRjZFLmiBdVymUH3wdHKe5q0=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6612n, cells: 36n }, + timeDelta: 4662n, + existingExtensions: [ + EXT.E1, + EXT.E2, + EXT.E3, + EXT.E4, + EXT.E5, + EXT.E6, + EXT.E7, + EXT.E8, + EXT.E9 + ] + }, + + expected: { + gasUsed: 7690n, + gasFee: 3_076_000n, + actionFee: 0n, + storageFee: 1751n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_884_551n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Remove Ext (FORK sibling)', + tag: 'remove-extension', + txHash: 'e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ69pAAAADEDgAwn98rwtMdTA2s8ZKvxso6TvVt1A6rLGwDYxmdDzUCkx+GSZ8tz1JN01ep+1qQ/KzjTqEiT3FmNUMJnOpxT/wcablE4fzDcJLezjzb5BrdZTBYtT0H3OOqLaJiIGzR54TIGfi5Y=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 6614n, cells: 36n }, + timeDelta: 511n, + existingExtensions: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7, EXT.E8] + }, + + expected: { + gasUsed: 7765n, + gasFee: 3_106_000n, + actionFee: 0n, + storageFee: 192n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_912_992n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Remove Ext (2→1)', + tag: 'remove-extension', + txHash: '4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8mYAAAAA0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM+ox14QmGm2ktksbaghTgG/OlpdAlQgbv1KoNQhwELXWy9RHsOIlCLmoVzoqKn7ElDbj0CMk4b2t9mGH0uqvgNDTc/mk=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5546n, cells: 25n }, + timeDelta: 14_042n, + existingExtensions: [EXT.FIRST, EXT.SECOND] + }, + + expected: { + gasUsed: 6565n, + gasFee: 2_626_000n, + actionFee: 0n, + storageFee: 3867n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_436_667n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + }, + + { + name: 'V5R1 - Remove Ext (1→0)', + tag: 'remove-extension', + txHash: '9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4', + + input: { + inMsgBoc: + 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8/uAAAABEDgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5TWBewP4woqHupijNFCZ3KWoGsCJ0hfRU2OiPO5sL27iJwfZBrjOT5CdY3zpWvkiPt9QklKXYWfpvJ1eUtusdARKRdmyQ=', + walletVersion: TonWalletVersion.V5R1, + storageUsed: { bits: 5280n, cells: 23n }, + timeDelta: 2880n, + existingExtensions: [EXT.FIRST] + }, + + expected: { + gasUsed: 5865n, + gasFee: 2_346_000n, + actionFee: 0n, + storageFee: 738n, + importFee: 806_800n, + fwdFeeRemaining: 0n, + walletFee: 3_153_538n + }, + + blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 + } +]; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts index d0d786151..f76fc540a 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts @@ -16,7 +16,22 @@ import { fileURLToPath } from 'url'; import { assertUnreachable, TonWalletVersion, FeeBlockchainConfig } from '../../compat'; import { CellStats } from '../../fees'; +export type FixtureTag = + | 'simple-transfer' + | 'deploy-transfer' + | 'multi-transfer' + | 'send-all' + | 'jetton-transfer' + | 'jetton-deploy-transfer' + | 'dedup-within-msg' + | 'dedup-cross-msg' + | 'library-body' + | 'add-extension' + | 'remove-extension'; + export type WalletFeeTestCase = { + name: string; + tag: FixtureTag; txHash: string; input: { inMsgBoc: string; // base64 encoded BOC diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts deleted file mode 100644 index a39a43e56..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-deploy-transfer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * V3R1 Deploy + Simple TON Transfer - * https://tonviewer.com/transaction/d43bd4a7a00ee3160cd266013020a126f5662094ed8b10fd54ea7ee56c64ef2d - * - * Wallet: UQAuxK2L3BqMiY8KOTxDYvLTWS64R6gD0Xh_Ar8MTOmaIjBa - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * Deploy: YES (seqno=0, StateInit included) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; -import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; - -// === Export === - -export const V3R1_DEPLOY_TRANSFER: WalletFeeTestCase = { - txHash: '56d703d5a575c1ebebc1ca4c4d53a0fe153868f1819e90dce5454aaa60f85cbe', - - input: { - inMsgBoc: - 'te6cckECBAEAAUsAA+GIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2EZAlKodvdHYSVEbp2r8c7Ovqg9QjJz6RTyYGGJmk1EbVcqKWiZ/4OROX1EHhkLSDTpPgt4wwrrxtrfORRdd+8qBlNTRi/////+AAAAAAcAECAwDA/wAg3SCCAUyXupcw7UTQ1wsf4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBkZXBsb3kgdGVzdCOmdwU=', - walletVersion: TonWalletVersion.V3R1, - storageUsed: UNINIT_ACCOUNT_STORAGE, - timeDelta: 25832n // 1765897809 - 1765871977 (~7.2h since funding) - }, - - expected: { - gasUsed: 2917n, - gasFee: 1_166_800n, - actionFee: 133_331n, - storageFee: 238n, - importFee: 1_182_400n, - fwdFeeRemaining: 266_669n, - walletFee: 2_749_438n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts deleted file mode 100644 index fcd297142..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-multi-transfer.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * V3R1 Multi-message TON Transfer (3 messages) - * https://tonviewer.com/transaction/a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943 - * - * Wallet: EQDxajExFHtCu7AxEu195inKr8ZkI9WDJCzhegKvu2kme2TM - * Destinations: 3 different addresses - * Value: 0.01 TON each - * seqno: 2 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount - * Expected: gasUsed = 2275 + 642*3 = 4201 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// === Export === - -export const V3R1_MULTI_TRANSFER: WalletFeeTestCase = { - txHash: 'a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943', - - input: { - inMsgBoc: - 'te6cckECBAEAAVUAA+OIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2A1v/MHLv+Ml1GqL+s0qf7DWsmXD5MHneAK5FevAbsHo1W8COSizPAKXbac9yyyJPQFvrtyShlcz9YX89cYpGuCFNTRi7ShM/YAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IxIG11bHRpIHRlc3QgMQCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjEgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBtdWx0aSB0ZXN0IDNwpkTh', - walletVersion: TonWalletVersion.V3R1, - storageUsed: { bits: 1195n, cells: 3n }, - timeDelta: 7261n // 1765959359 (utime) - 1765952098 (last_paid) - }, - - expected: { - gasUsed: 4201n, // 2275 + 642*3 - gasFee: 1_680_400n, - actionFee: 399_993n, // 3 messages - storageFee: 299n, - importFee: 1_211_200n, - fwdFeeRemaining: 800_007n, - walletFee: 4_091_899n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts deleted file mode 100644 index a7a51e6c7..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r1-simple-transfer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * V3R1 Simple TON Transfer - * https://tonviewer.com/transaction/9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3 - * - * Wallet: EQDxajExFHtCu7AxEu195inKr8ZkI9WDJCzhegKvu2kme2TM - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * seqno: 1 (NOT deploy - no StateInit) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V3R1_SIMPLE_TRANSFER: WalletFeeTestCase = { - txHash: '9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3', - - input: { - inMsgBoc: - 'te6cckEBAgEAvAAB34gB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPYBb9MXN6TWOd2BFH0MHHC8e7AbH0XaKpn2ViX8n4vM4b6FN4c0n7CPo8ajbuNmDsu8CxTI7dNwXlW2Rq6V5GEwaU1NGLtKElyAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMSBzaW1wbGUgdGVzdOJYMOI=', - walletVersion: TonWalletVersion.V3R1, - storageUsed: { bits: 1195n, cells: 3n }, - timeDelta: 54291n // 1765952100 (send @ 13:15) - 1765897809 (last_paid) - }, - - expected: { - gasUsed: 2917n, - gasFee: 1_166_800n, - actionFee: 133_331n, - storageFee: 2233n, - importFee: 667_200n, - fwdFeeRemaining: 266_669n, - walletFee: 2_236_233n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts deleted file mode 100644 index d5537b015..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-deploy-transfer.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * V3R2 Deploy + Simple TON Transfer - * https://tonviewer.com/transaction/c222ab3fd903f3e14e89f571d7fc4662036150381675b31f14dc62eb7955abae - * - * Wallet: UQCscC8Yeutc-J4LJFsRsFsUKM8qerLuCx7TRUl9tjqVLYym - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * Deploy: YES (seqno=0, StateInit included) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; -import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; - -export const V3R2_DEPLOY_TRANSFER: WalletFeeTestCase = { - txHash: 'a3c4513865506e14d8eb05e0c2e508827125d615384bbb1f91b19c2147088c99', - - input: { - inMsgBoc: - 'te6cckECBAEAAVoAA+GIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaEYeccEtypc03z0Rh18v2sNOdvavY009n1UlBH0rd5oZPI7LZzgEdRGHhsFEwt5WlK8Okip/eG7g6a/GWGbNcceAFNTRi/////+AAAAAAcAECAwDe/wAg3SCCAUyXuiGCATOcurGfcbDtRNDTH9MfMdcL/+ME4KTyYIMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOjRAaTIyx/LH8v/ye1UAFAAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBkZXBsb3kgdGVzdKcWeY4=', - walletVersion: TonWalletVersion.V3R2, - storageUsed: UNINIT_ACCOUNT_STORAGE, - timeDelta: 25861n // 1765897812 - 1765871951 (~7.2h since funding) - }, - - expected: { - gasUsed: 2994n, - gasFee: 1_197_600n, - actionFee: 133_331n, - storageFee: 238n, - importFee: 1_230_400n, - fwdFeeRemaining: 266_669n, - walletFee: 2_828_238n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts deleted file mode 100644 index 9c3282c67..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-multi-transfer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * V3R2 Multi-message TON Transfer (3 messages) - * https://tonviewer.com/transaction/9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298 - * - * Wallet: EQBFk5RG37Dt-23Egx2RfJ2IHU_Kg99Wh0geO8Kf2V-VrSXr - * Destinations: 3 different addresses - * Value: 0.01 TON each - * seqno: 2 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount - * Expected: gasUsed = 2352 + 642*3 = 4278 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V3R2_MULTI_TRANSFER: WalletFeeTestCase = { - txHash: '9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298', - - input: { - inMsgBoc: - 'te6cckECBAEAAVUAA+OIAIsnKI2/Ydv224kGOyL5OxA6n5UHvq0OkDx3hT+yvytaB34zOGFqj3w0evXwQI/ANdZwbVqjHPreRk+zMd9RNGEWZydn2T1g68qTAxs5RMAAdzQn5p/x+A5v57KYgvgzkFFNTRi7ShOIgAAAABAYGBwBAgMAkGIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWM1IyIG11bHRpIHRlc3QgMQCQYgB4tRiYij2hXdgYiXa+8xTlV+MyEerBkhZwvQFX3bSTPZzEtAAAAAAAAAAAAAAAAAAAAAAAAFYzUjIgbXVsdGkgdGVzdCAyAJBiAHZvQ5onmVjHlys7khXWibBVj8TTEgE6DMssxFzEr+h1nMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBtdWx0aSB0ZXN0IDPXKVZl', - walletVersion: TonWalletVersion.V3R2, - storageUsed: { bits: 1315n, cells: 3n }, - timeDelta: 1101n // 1765960460 (utime) - 1765959359 (last_paid) - }, - - expected: { - gasUsed: 4278n, // 2352 + 642*3 - gasFee: 1_711_200n, - actionFee: 399_993n, - storageFee: 48n, - importFee: 1_211_200n, - fwdFeeRemaining: 800_007n, - walletFee: 4_122_448n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts deleted file mode 100644 index 779c333db..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v3r2-simple-transfer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * V3R2 Simple TON Transfer - * https://tonviewer.com/transaction/2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7 - * - * Wallet: EQBFk5RG37Dt-23Egx2RfJ2IHU_Kg99Wh0geO8Kf2V-VrSXr - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * seqno: 1 (NOT deploy - no StateInit) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V3R2_SIMPLE_TRANSFER: WalletFeeTestCase = { - txHash: '2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7', - - input: { - inMsgBoc: - 'te6cckEBAgEAvAAB34gAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1oGN2uEUMoi+OFETECx05z1AFr7rFZqUgxapDpSA0ZyN4et1EkPIow8h6Dwqgw/NXaa33DrEQJp9WT5aouP4q54SU1NGLtKEqPAAAAACBwBAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjNSMiBzaW1wbGUgdGVzdI5CrMA=', - walletVersion: TonWalletVersion.V3R2, - storageUsed: { bits: 1315n, cells: 3n }, - timeDelta: 56575n // 1765954387 (utime) - 1765897812 (last_paid) - }, - - expected: { - gasUsed: 2994n, - gasFee: 1_197_600n, - actionFee: 133_331n, - storageFee: 2_431n, - importFee: 667_200n, - fwdFeeRemaining: 266_669n, - walletFee: 2_267_231n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts deleted file mode 100644 index 635e9851f..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-deploy-transfer.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * V4R2 Deploy + Simple TON Transfer - * https://tonviewer.com/transaction/9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6 - * - * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpkQCdBlZWYi5iV_Q6-tP - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * Deploy: YES (seqno=0, StateInit included) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; -import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; - -export const V4R2_DEPLOY_TRANSFER: WalletFeeTestCase = { - txHash: '9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6', - - input: { - inMsgBoc: - 'te6cckECFwEAA78AA+OIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWEZR9QrhoVohsiDIWH/WY1dxOdqwnAvf+v3obhBMYcdEyVVSqyAXvBIhl3JveXrQW/n7NFBiStGX16/QSgf/ZawHlNTRi/////+AAAAAAAHABFRYBFP8A9KQT9LzyyAsCAgEgAxACAUgEBwLm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQUGAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAgPAgEgCQ4CAVgKCwA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIAwNABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xESExQAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF4jE4Brs3Pa3f2iTWmht+POgbEVqVeRb6lTT5vquGk3cQACOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgZGVwbG95IHRlc3QU7ERC', - walletVersion: TonWalletVersion.V4R2, - storageUsed: UNINIT_ACCOUNT_STORAGE, - timeDelta: 29189n // 1765901103 - 1765871914 (real tx utime) - }, - - expected: { - gasUsed: 3308n, - gasFee: 1_323_200n, - actionFee: 133_331n, - storageFee: 269n, - importFee: 3_740_000n, - fwdFeeRemaining: 266_669n, - walletFee: 5_463_469n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts deleted file mode 100644 index 015f18b87..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-multi-transfer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * V4R2 Multi-message TON Transfer (3 messages) - * https://tonviewer.com/transaction/f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d - * - * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpiQCdBmWWYi5iV_Q62Yc - * Destinations: 3 different addresses - * Value: 0.01 TON each - * seqno: 2 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount - * Expected: gasUsed = 2666 + 642*3 = 4592 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V4R2_MULTI_TRANSFER: WalletFeeTestCase = { - txHash: 'f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d', - - input: { - inMsgBoc: - 'te6cckECBAEAAVYAA+WIAdm9DmieZWMeXKzuSFdaJsFWPxNMSAToMyyzEXMSv6HWAWW792PeJs2k3RpePkQV3QSb7A76RXTzp7YyH7Hl3TE5Xl23gFlLONfJWsx/ruBNK24WPIX/mjYtHdgkvYuGSAFNTRi7ShN44AAAABAAGBgcAQIDAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjRSMiBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNFIyIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgbXVsdGkgdGVzdCAzaX8lDA==', - walletVersion: TonWalletVersion.V4R2, - storageUsed: { bits: 5689n, cells: 22n }, - timeDelta: 650n // 1765961110 (utime) - 1765960460 (last_paid) - }, - - expected: { - gasUsed: 4592n, // 2666 + 642*3 - gasFee: 1_836_800n, - actionFee: 399_993n, - storageFee: 166n, - importFee: 1_211_200n, - fwdFeeRemaining: 800_007n, - walletFee: 4_248_166n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts deleted file mode 100644 index 5e9e834af..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v4r2-simple-transfer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * V4R2 Simple TON Transfer - * https://tonviewer.com/transaction/da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11 - * - * Wallet: EQDs3oc0TzKxjy5WdyQrrRNgqx-JpiQCdBmWWYi5iV_Q62Yc - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * seqno: 1 (NOT deploy - no StateInit) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V4R2_SIMPLE_TRANSFER: WalletFeeTestCase = { - txHash: 'da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11', - - input: { - inMsgBoc: - 'te6cckEBAgEAvQAB4YgB2b0OaJ5lYx5crO5IV1omwVY/E0xIBOgzLLMRcxK/odYA/yGGDBicSwhIT4Px0xBMWKKXCRIh4qn6VaY0lmXkTTLKlvg+tIAxGf5fP04JxrwPT9EPGvHt/vwE+TsvkouoQU1NGLtKErSgAAAACAAcAQCOYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAFY0UjIgc2ltcGxlIHRlc3QPg4u7', - walletVersion: TonWalletVersion.V4R2, - storageUsed: { bits: 5689n, cells: 22n }, - timeDelta: 53817n // 1765954920 (utime) - 1765901103 (last_paid) - }, - - expected: { - gasUsed: 3308n, - gasFee: 1_323_200n, - actionFee: 133_331n, - storageFee: 13_705n, - importFee: 667_200n, - fwdFeeRemaining: 266_669n, - walletFee: 2_404_105n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts deleted file mode 100644 index b5263929e..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-cross-msg.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * V5R1 Cross-message Deduplication Test (3.2) - * https://tonviewer.com/transaction/3dbbc6f071680ce7d609c4799d6fcea100161b88275a4102a4e37522f88703e3 - * - * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy - * Destinations: 3 different addresses - * Value: 0.01 TON each - * Body: Same comment cell for all messages (deduplicated) - * seqno: 3 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Test 3.2 - Validate cell deduplication ACROSS multiple outgoing messages. - * When multiple messages share the same body cell, it should be counted only once - * in importFee calculation (inMsg uses shared visited Set). - * - * Expected: importFee < v5r1-multi-transfer.importFee (1,419,200) due to deduplication - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_DEDUP_CROSS_MSG: WalletFeeTestCase = { - txHash: '139b5f7210fc0a86b54f447d0d060b1d843c1f52bf925057a7011461219a72c4', - - input: { - inMsgBoc: - 'te6cckECCAEAAVwAAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShSOAAAAABzvOnnqwdRZiru+O8FlP0SOyPOk4d6GXn7lXwnoaPTbtHOFftayjNBC100WUm8nT68UbS91TZ/W/I4vkTl8qVoXAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAIJiAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFnMS0AAAAAAAAAAAAAAAAAAAAAAAAZGVkdXAgdGVzdACCYgAU4UYSo9L88Nr0WUdUVlBZhGzexS5eUgC5c3p4TWIbLRzEtAAAAAAAAAAAAAAAAAAAAAAAAGRlZHVwIHRlc3QAgmIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUcxLQAAAAAAAAAAAAAAAAAAAAAAABkZWR1cCB0ZXN0/q/S9A==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 1125n // 1765970307 (actual) - 1765969182 (last_paid) - }, - - expected: { - gasUsed: 6373n, // 4222 + 717*3 - gasFee: 2_549_200n, - actionFee: 399_993n, // Each outMsg counted separately (no cross-msg dedup for actionFee) - storageFee: 275n, - importFee: 1_352_000n, // vs 1,419,200 in multi-transfer (~5% savings from body dedup) - fwdFeeRemaining: 800_007n, - walletFee: 5_101_475n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts deleted file mode 100644 index a9c092ef3..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-dedup-within-msg.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * V5R1 Within-message Deduplication Test (3.1) - * https://tonviewer.com/transaction/440d91a1e727fa59efbe420dc747b265a284ae4bb2d7d8b0e4919ea16e2b0965 - * - * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy - * Destination: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL - * Value: 0.01 TON - * Body: Custom cell with 2 refs to the SAME cell - * seqno: 2 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Test 3.1 - Validate cell deduplication WITHIN a single message. - * The message body has 2 references to the same cell (same hash). - * countUniqueCellStats should count it as 2 cells (body + 1 shared), not 3. - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_DEDUP_WITHIN_MSG: WalletFeeTestCase = { - txHash: '0339b0c0720456038ecfcaedc19f287341cd2df5ee0891321540070da554d054', - - input: { - inMsgBoc: - 'te6cckEBBQEA3AAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFGrAAAAAFbwDwxgL1Ime6rKH1Otm8lnAWH3J8aUH6Jw3sySXW+Lc/ZpaJ1qchT/sGJMcuaW5TIlZ1QK04gXiCtkHKo31Ih0BAgoOw8htAwIDAAACbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAEBAA8EjRWeHNoYXJlZCBkYXRhIGZvciBkZWR1cCB0ZXN0je6JOA==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 2797n // 1765969182 (actual) - 1765966385 (last_paid) - }, - - expected: { - gasUsed: 4939n, // 4222 + 717*1 - gasFee: 1_975_600n, - actionFee: 178_663n, // Body with 2 refs to same cell (deduplicated) - storageFee: 684n, - importFee: 848_000n, - fwdFeeRemaining: 357_337n, - walletFee: 3_360_284n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts deleted file mode 100644 index 04eb234bf..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-deploy-transfer.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * V5R1 Deploy + Simple TON Transfer - * https://tonviewer.com/transaction/8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb - * - * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ysoV - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * Deploy: YES (seqno=0, StateInit included) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; -import { UNINIT_ACCOUNT_STORAGE } from '../../fees'; - -export const V5R1_DEPLOY_TRANSFER: WalletFeeTestCase = { - txHash: '8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb', - - input: { - inMsgBoc: - 'te6cckECGQEAA3kAA+eIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUEY5tLO3P///iP////+AAAAAXRMD9rgTSL3siUO+VNqBj+rGQeQcxIl4tznudt+z+R7es1YgWsujT2Y5/87YkjeecfaLUxVnEk9b9Q4Oj8WD4RAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iMRicA12bntbv7RJrTQ2/HnQNiK1KvIt9Spp831XDSbuIAIKDsPIbQMXGAAAAI5iABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBkZXBsb3kgdGVzdB3rhBA=', - walletVersion: TonWalletVersion.V5R1, - storageUsed: UNINIT_ACCOUNT_STORAGE, - timeDelta: 5014n // 1765901103 - 1765896089 (real tx utime) - }, - - expected: { - gasUsed: 4939n, - gasFee: 1_975_600n, - actionFee: 133_331n, - storageFee: 47n, - importFee: 3_565_200n, - fwdFeeRemaining: 266_669n, - walletFee: 5_940_847n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts deleted file mode 100644 index bba78ce3d..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-eighth.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * V5R1 Add Eighth Extension - * https://tonviewer.com/transaction/c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729 - * - * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 - * Extension: 0:0000000000000000000000000000000000000000000000000000000000000004 - * seqno: 9 - * utime: 1765995008 - * - * Purpose: Test 5.3 - Validate fee estimation for adding extension - * when there are 7 existing extensions. - * - * Patricia trie analysis (7 existing extensions): - * - 613f... (0110...), 4758... (0100...), 5f70... (0101...) → share prefix "01" - * - ba6e... (1011...), ffff01-03 (1111...) → bit 0 = '1' - * - * New key 0000...04 (0000...) has bit 0 = '0', bit 1 = '0'. - * Path traversal: root → left subtree (prefix "01") - * pathDepth=1, subtree has 3 elements (>1) - * - * Gas calculation (cellLoads=2): - * gasUsed = 6610 + 600 × 2 = 7810 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 7 extensions (in order they were added) -const EXISTING_EXTENSIONS = [ - '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 - 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 - '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 - '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03' // #7 -]; - -export const V5R1_EXTENSION_ADD_EIGHTH: WalletFeeTestCase = { - txHash: 'c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQvtYAAAACUCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmd3fOQSyMWUSZO4N5SeZsrdUp2AjWLKBkD1H2kJyfIpAVO7JhYFrgxCAq1IJNy8AU5vgo+0Yz5AP+MQaG8UvgZGDSb/I=', - existingExtensions: EXISTING_EXTENSIONS, // 7 existing extensions - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 6349n, cells: 34n }, - timeDelta: 14098n // utime(1765995008) - last_paid(1765980910) - }, - - expected: { - gasUsed: 7810n, // cellLoads=2: 6610 + 600 × 2 = 7810 - gasFee: 3_124_000n, - actionFee: 0n, // ZERO because no outMsgs - storageFee: 5023n, - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_935_823n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts deleted file mode 100644 index 5d48477f1..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-first.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * V5R1 Add First Extension - * https://tonviewer.com/transaction/0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a - * - * Wallet: EQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-6q9 - * Extension: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 - * seqno: 1 - * utime: 1766032992 - * - * Purpose: Test 5.1 - Validate fee estimation for extension actions - * where there are NO outMsgs, only internal wallet dictionary operations. - * - * Key difference from transfers: - * - outMsgs: [] (empty - no outgoing messages) - * - actionFee: 0 (no forward fees to calculate) - * - gasUsed: 6110 (first extension: baseGas + overhead + CELL_WRITE) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_EXTENSION_ADD_FIRST: WalletFeeTestCase = { - txHash: '0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5RwAAAAAUCgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QvNbREC1rFe++nNUEYL7i/jFMan6sWaAcQkLcFhMQRdkdHH5FaquHUva9+ECMZuspGMjlVV45P48fwaRk8G6gDO+OSV0=', - existingExtensions: [], // First extension: empty dict - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 1734n // utime(1766032992) - last_paid(1766031258) - }, - - expected: { - gasUsed: 6110n, // First extension: baseGas(4222) + overhead(1388) + CELL_WRITE(500) - gasFee: 2_444_000n, - actionFee: 0n, // ZERO because no outMsgs - storageFee: 424n, - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_251_224n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts deleted file mode 100644 index 7056e6591..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-ninth.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * V5R1 Add Ninth Extension - * https://tonviewer.com/transaction/6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331 - * - * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 - * Extension: 0:0000000000000000000000000000000000000000000000000000000000000005 - * seqno: 10 - * utime: 1766038160 - * - * Purpose: Test extension addition with 8 existing extensions. - * Adding 9th extension (00000005) which neighbors 8th (00000004). - * - * Patricia trie analysis: - * - 8 existing extensions with #8 being 00000004 (0000...0100) - * - New key 00000005 (0000...0101) differs from #8 at bit 254 - * - Path: root → 0 prefix → 00 prefix → deep fork with 00000004 - * - * Gas calculation (cellLoads=3): - * gasUsed = 6610 + 600 × 3 = 8410 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 8 extensions (in order they were added) -const EXISTING_EXTENSIONS = [ - '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 - 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 - '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 - '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 - '0000000000000000000000000000000000000000000000000000000000000004' // #8 -]; - -export const V5R1_EXTENSION_ADD_NINTH: WalletFeeTestCase = { - txHash: '6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ7IoAAAACkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqOFBZ7pjTJNQIOWc/u9QNef9HkzpTWRow+9tis9REdamv1d6G7HLupjHN3bKbz+z5OUbPirI0MZ2KSXbfF2v4HJnWqds=', - existingExtensions: EXISTING_EXTENSIONS, // 8 existing extensions - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 6614n, cells: 36n }, - timeDelta: 43152n // utime(1766038160) - last_paid(1765995008) - }, - - expected: { - gasUsed: 8410n, // cellLoads=3: 6610 + 600 × 3 = 8410 - gasFee: 3_364_000n, - actionFee: 0n, // ZERO because no outMsgs - storageFee: 16_208n, - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 4_187_008n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts deleted file mode 100644 index 7585429ef..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-extension-add-second.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * V5R1 Add Second Extension - * https://tonviewer.com/transaction/30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11 - * - * Wallet: EQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-6q9 - * Extension: EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd99 - * seqno: 2 - * utime: 1766034599 - * - * Purpose: Test 5.2 - Validate fee estimation for adding extension - * when there is already 1 existing extension. - * - * With 1 existing extension, trie is ROOT → LEAF. - * Adding second extension requires loading root (1 cellLoad), - * then creating fork at point of key divergence. - * - * Gas calculation (cellLoads=1): - * gasUsed = 6610 + 600 × 1 = 7210 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing extension (from first test) -// Address: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 -const EXISTING_EXTENSION_HASH = 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca'; - -export const V5R1_EXTENSION_ADD_SECOND: WalletFeeTestCase = { - txHash: '30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ5nrAAAAAkCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOMgSji7NZxA4UOBTvhF0xkwL27g64NPxmjxPQtAnno9A3oGEnAO11p+ilnMrEajHbiV65pR/ZZoWlLzqcrtAILHFWRfg=', - existingExtensions: [EXISTING_EXTENSION_HASH], // 1 existing extension - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5280n, cells: 23n }, - timeDelta: 1607n // utime(1766034599) - last_paid(1766032992) - }, - - expected: { - gasUsed: 7210n, // cellLoads=1: 6610 + 600 × 1 = 7210 - gasFee: 2_884_000n, - actionFee: 0n, // ZERO because no outMsgs - storageFee: 412n, - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_691_212n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts deleted file mode 100644 index 9c417f331..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-deploy-transfer.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * V5R1 Deploy + Jetton Transfer (POSASYVAET) - * https://tonviewer.com/transaction/4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab - * - * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy - * Jetton: POSASYVAET (EQBR-4-x7dik6UIHSf_IE6y2i7LdPrt3dLtoilA8sObIquW8) - * Recipient: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL - * Deploy: YES (seqno=0, StateInit included) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate fee calculation for jetton transfer with wallet deployment - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_JETTON_DEPLOY_TRANSFER: WalletFeeTestCase = { - txHash: '4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab', - - input: { - inMsgBoc: - 'te6cckECGgEAA70AA+eIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6EY5tLO3P///iP////+AAAAAUXT6I0OuBJ/Jj8s0J5LMQqDo1L3ljAh2adGyCpls7/OX2XlaRAhVWxGZN+2wThSA7EULv22lQcfiEksiBwREQXAEVFgEU/wD0pBP0vPLICwICASADDgIBSAQFAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hEQAgEgBg0CASAHCgIBbggJABmtznaiaEAg65Drhf/AABmvHfaiaEAQ65DrhY/AAgFICwwAF7Ml+1E0HHXIdcLH4AARsmL7UTQ1woAgABm+Xw9qJoQICg65D6AsAQLyDwEeINcLH4IQc2lnbrry4Ip/EAHmjvDtou37IYMI1yICgwjXIyCAINch0x/TH9Mf7UTQ0gDTHyDTH9P/1woACvkBQMz5EJoolF8K2zHh8sCH3wKzUAew8tCEUSW68uCFUDa68uCG+CO78tCIIpL4AN4BpH/IygDLHwHPFsntVCCS+A/ecNs82BED9u2i7fsC9AQhbpJsIY5MAiHXOTBwlCHHALOOLQHXKCB2HkNsINdJwAjy4JMg10rAAvLgkyDXHQbHEsIAUjCw8tCJ10zXOTABpOhsEoQHu/Lgk9dKwADy4JPtVeLSAAHAAJFb4OvXLAgUIJFwlgHXLAgcEuJSELHjDyDXShITFACWAfpAAfpE+Cj6RDBYuvLgke1E0IEBQdcY9AUEnX/IygBABIMH9FPy4IuOFAODB/Rb8uCMItcKACFuAbOw8tCQ4shQA88WEvQAye1UAHIw1ywIJI4tIfLgktIA7UTQ0gBRE7ry0I9UUDCRMZwBgQFA1yHXCgDy4I7iyMoAWM8Wye1Uk/LAjeIAEJNb2zHh10zQAFGAAAAAP///iKJlC4pYpKjPnRwcPVBteZ/STb08sAm+NV8fKC+V2lhkoAIKDsPIbQMXGAAAAWhiAG86P6y6gCm5XLH3xO0tXwqnOO8/zrgSt4FIXPzeWbKfIBfXhAAAAAAAAAAAAAAAAAABGQCoD4p+pe5w+sQAAAABOYloCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICvS+pMw==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 103n, cells: 1n }, - timeDelta: 1454n // 1765965604 - 1765964150 (real tx utime - last_paid) - }, - - expected: { - gasUsed: 4939n, - gasFee: 1_975_600n, - actionFee: 236_263n, - storageFee: 14n, - importFee: 3_813_200n, - fwdFeeRemaining: 472_537n, - walletFee: 6_497_614n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts deleted file mode 100644 index 0670c4678..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-jetton-simple-transfer.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * V5R1 Simple Jetton Transfer (USDT) - * https://tonviewer.com/transaction/e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70 - * - * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy - * Jetton: USDT (EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs) - * Recipient: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL - * Deploy: NO (seqno=1, no StateInit) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate fee calculation for jetton transfer without wallet deployment - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_JETTON_SIMPLE_TRANSFER: WalletFeeTestCase = { - txHash: 'e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70', - - input: { - inMsgBoc: - 'te6cckECBQEAAQ4AAeWIAAPOI+mE47pQOyxdNKF/avgxvWh9YbJ4Tcg7luVXDla6A5tLO3P///iLShQTaAAAAAwLgww5HSimZ7XvQOb7PfbRnm5uaH32GbNFdbb+ELWLhZwkUvP/wjlKG907QVOAUwG/8fakYMT0E2B3uE2vEAwfAQIKDsPIbQMCAwAAAWhiAFC9XmrYDrIpnVpvUkWdEZgLV9Lg+7dVJpKDrftKYkjqIBfXhAAAAAAAAAAAAAAAAAABBACoD4p+pe5w+sQAAAACMBhqCAE09rrD6bznPlKIejHNcWwNeRhPjI4FhxmqtwCTGqAlFwAAecR9MJx3Sgdli6aUL+1fBjetD6w2Twm5B3LcquHK10ICmd9p7Q==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 781n // 1765966385 (real utime) - 1765965604 (last_paid) - }, - - expected: { - gasUsed: 4939n, - gasFee: 1_975_600n, - actionFee: 236_263n, - storageFee: 191n, - importFee: 1_011_200n, - fwdFeeRemaining: 472_537n, - walletFee: 3_695_791n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts deleted file mode 100644 index bdc7066cd..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-library-body.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * V5R1 Library Cell in Message Body Test (3.3) - * https://tonviewer.com/transaction/743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3 - * - * Wallet: UQAB5xH0wnHdKB2WLppQv7V8GN60PrDZPCbkHctyq4crXbYy - * Destination: UQCae11h9N5znylEPRjmuLYGvIwnxkcCw4zVW4BJjVASi5eL - * Value: 0.01 TON - * Body: Contains library reference cell (264 bits, exotic) - * seqno: 4 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Test 3.3 - Validate that fee estimator correctly handles - * library reference cells (exotic cells) in message body. - * Library cell should be counted as 264 bits (8-bit type + 256-bit hash), - * NOT dereferenced to its full content. - * - * Library used: USDT Jetton Wallet Code - * Hash: 8f452d7a4dfd74066b682365177259ed05734435be76b5fd4bd5d8af2b7c3d68 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_LIBRARY_BODY: WalletFeeTestCase = { - txHash: '743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3', - - input: { - inMsgBoc: - 'te6cckEBBQEA3gAB5YgAA84j6YTjulA7LF00oX9q+DG9aH1hsnhNyDuW5VcOVroDm0s7c///+ItKFOcIAAAAJc98QMupXJWIfxDzyTVwthc5JpV/rwl0fMK9iENFr6Lk2KCeFQ4Ioplf5MoYJBn7/Zpbm1jgrauI3y6cNNdErAMBAgoOw8htAwIDAAABbmIATT2usPpvOc+Uoh6Mc1xbA15GE+MjgWHGaq3AJMaoCUWcxLQAAAAAAAAAAAAAAAAAAAAAAAAECEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWhIvBQ+', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 2792n // utime=1765973099, last_paid=1765970307 - }, - - expected: { - gasUsed: 4939n, // 4222 + 717*1 - gasFee: 1_975_600n, - actionFee: 181_863n, // includes 264 bits from library cell - storageFee: 683n, - importFee: 857_600n, - fwdFeeRemaining: 363_737n, - walletFee: 3_379_483n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts deleted file mode 100644 index 683f58476..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-multi-transfer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * V5R1 Multi-message TON Transfer (3 messages) - * https://tonviewer.com/transaction/9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a - * - * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 - * Destinations: 3 different addresses - * Value: 0.01 TON each - * seqno: 2 - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - * - * Purpose: Validate gas formula gasUsed = baseGas + gasPerMsg * outMsgsCount - * Expected: gasUsed = 4222 + 717*3 = 6373 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_MULTI_TRANSFER: WalletFeeTestCase = { - txHash: '9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a', - - input: { - inMsgBoc: - 'te6cckECCAEAAXEAAeWIAdFezAkBlq1Gq8DZ+aRZ6OhKWuQ9l5ZlezH5kKRW3XeUA5tLO3P///iLShO3YAAAABUZ50fwccMDqEOvwu6DVhQAQOt4PsclSdfdktQsk5nDCob7nMpPMfCMCQeCBX297wU0F9hpGIJYOAsjb/Nx8RoTAQIKDsPIbQMCBwIKDsPIbQMDBgIKDsPIbQMEBQAAAJBiABThRhKj0vzw2vRZR1RWUFmEbN7FLl5SALlzenhNYhstHMS0AAAAAAAAAAAAAAAAAAAAAAAAVjVSMSBtdWx0aSB0ZXN0IDEAkGIAeLUYmIo9oV3YGIl2vvMU5VfjMhHqwZIWcL0BV920kz2cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIG11bHRpIHRlc3QgMgCQYgAiycojb9h2/bbiQY7Ivk7EDqflQe+rQ6QPHeFP7K/K1pzEtAAAAAAAAAAAAAAAAAAAAAAAAFY1UjEgbXVsdGkgdGVzdCAzCkLbdA==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 6338n // 1765961799 (utime) - 1765955461 (last_paid) - }, - - expected: { - gasUsed: 6373n, // 4222 + 717*3 - gasFee: 2_549_200n, - actionFee: 399_993n, - storageFee: 1_549n, - importFee: 1_419_200n, - fwdFeeRemaining: 800_007n, - walletFee: 5_169_949n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts deleted file mode 100644 index cf61cdb58..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-fork-sibling.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * V5R1 Remove Extension - FORK Sibling Case - * https://tonviewer.com/transaction/dd42b5e585766b58a1e4cb1d24756d1ec4351c1f5a244a44fba1d9a8ba3f2f38 - * in_msg.hash: e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b - * - * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 - * Extension to remove: 0:613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526 (#1) - * seqno: 12 (after removing #9 with seqno=11) - * - * Purpose: Test remove extension gas calculation with FORK sibling. - * - * REMOVE gas formula: - * gas = 5290 + 600×cellLoads + (needsEdgeMerge ? 75 : 0) - * where needsEdgeMerge = siblingIsFork || rootCollapse(2→1) - * - * When removing #1 (613fbe57...) from 8 extensions: - * - cellLoads = 4 (root → 0-branch → fork → leaf) - * - sibling = FORK containing #3 and #4 → siblingIsFork = true - * - needsEdgeMerge = true → +75 gas - * - gas = 5290 + 2400 + 75 = 7765 ✓ - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 8 extensions after #9 was removed (state after seqno=11) -const EXISTING_EXTENSIONS = [ - '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 - to be removed - 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 - '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 (sibling branch) - '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 (sibling branch) - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 - '0000000000000000000000000000000000000000000000000000000000000004' // #8 -]; - -export const V5R1_REMOVE_EXT_FORK_SIBLING: WalletFeeTestCase = { - txHash: 'e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ69pAAAADEDgAwn98rwtMdTA2s8ZKvxso6TvVt1A6rLGwDYxmdDzUCkx+GSZ8tz1JN01ep+1qQ/KzjTqEiT3FmNUMJnOpxT/wcablE4fzDcJLezjzb5BrdZTBYtT0H3OOqLaJiIGzR54TIGfi5Y=', - existingExtensions: EXISTING_EXTENSIONS, - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 6614n, cells: 36n }, - timeDelta: 511n // utime(1766043333) - last_paid(1766042822) - }, - - expected: { - // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork ? 75 : 0) - // With 8 extensions, removing #1: - // - cellLoads = 4 (root → 0-branch → fork → leaf) - // - sibling = FORK (contains #3, #4) - // - gas = 5290 + 600×4 + 75 = 7765 ✓ - gasUsed: 7765n, - gasFee: 3_106_000n, // 7765 × 400 - actionFee: 0n, - storageFee: 192n, // ceil((6614×1 + 36×500) × 511 / 65536) - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_912_992n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts deleted file mode 100644 index 64598a2df..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-last.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * V5R1 Remove Last Extension (1→0) - * https://tonviewer.com/transaction/9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4 - * - * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 - * Extension to remove: 0:e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca - * seqno: 4 - * utime: 1766051521 - * - * Purpose: Test remove extension gas when dict becomes empty (1→0). - * - * REMOVE gas formula (from TVM dict_delete + W5 contract analysis): - * gas = 5290 + 600×cellLoads - 25 (for 1→0 only) - * - * With 1 extension (single LEAF): - * - cellLoads = 1 (read the single leaf before removing) - * - Result dict = null → store_dict(null) doesn't need cell_reload for ref - * - gas = 5290 + 600 - 25 = 5865 - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 1 extension (before removal) -const EXISTING_EXTENSIONS = [ - 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca' // the last one - to be removed -]; - -export const V5R1_REMOVE_EXT_LAST: WalletFeeTestCase = { - txHash: '9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8/uAAAABEDgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5TWBewP4woqHupijNFCZ3KWoGsCJ0hfRU2OiPO5sL27iJwfZBrjOT5CdY3zpWvkiPt9QklKXYWfpvJ1eUtusdARKRdmyQ=', - existingExtensions: EXISTING_EXTENSIONS, - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5280n, cells: 23n }, - timeDelta: 2880n // utime(1766051521) - last_paid(1766048641) - }, - - expected: { - // Formula: gas = 5290 + 600×cellLoads - 25 - // cellLoads = 1 (read single leaf) - // -25: store_dict(null) doesn't need cell_reload for reference - // gas = 5290 + 600 - 25 = 5865 - gasUsed: 5865n, - gasFee: 2_346_000n, // 5865 × 400 - actionFee: 0n, - storageFee: 738n, // ceil((5280×1 + 23×500) × 2880 / 65536) - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_153_538n // 2_346_000 + 738 + 806_800 - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts deleted file mode 100644 index 0c699918b..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-leaf-sibling.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * V5R1 Remove Extension - LEAF Sibling Case - * https://tonviewer.com/transaction/6da5c202e99d1d37bd7815c0392044822a3b6384d213a0d8e9cc4010edb6b676 - * in_msg.hash: 7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5 - * - * Wallet: UQD3KlCnEgNeGs4blSjo03JGyS4Rn1QiWhO7H6hcxaZwpAH6 - * Extension to remove: 0:0000000000000000000000000000000000000000000000000000000000000005 (#9) - * seqno: 11 - * - * Purpose: Test remove extension gas calculation with LEAF sibling. - * - * REMOVE gas formula: - * gas = 5290 + 600×cellLoads + (needsEdgeMerge ? 75 : 0) - * where needsEdgeMerge = siblingIsFork || rootCollapse(2→1) - * - * When removing #9 (0000...05): - * - cellLoads = 4 (root → 0-branch → 00-fork → leaf) - * - sibling = LEAF #8 (0000...04) → siblingIsFork = false - * - needsEdgeMerge = false → +0 gas - * - gas = 5290 + 2400 + 0 = 7690 ✓ - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 9 extensions (before removal) -const EXISTING_EXTENSIONS = [ - '613fbe5785a63a981b59e3255f8d94749deadba81d5658d806c6333a1e6a0526', // #1 - 'ba6ede4924bdc9ecbd4582c10bfacf1dfdf3e4f1bde5796819e45ff6ea0f8522', // #2 - '4758697a8b9cadbecfe0f102132435465768798a9bacbdcedff0011223344556', // #3 - '5f708192a3b4c5d6e7f8091a2b3c4d5e6f8091a2b3c4d5e6f708192a3b4c5d6e', // #4 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01', // #5 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02', // #6 - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03', // #7 - '0000000000000000000000000000000000000000000000000000000000000004', // #8 (sibling - LEAF) - '0000000000000000000000000000000000000000000000000000000000000005' // #9 - to be removed -]; - -export const V5R1_REMOVE_EXT_LEAF_SIBLING: WalletFeeTestCase = { - txHash: '7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgB7lShTiQGvDWcNypR0abkjZJcIz6oRLQndj9QuYtM4UgMAQDlc2lnbn///xFpQ61cAAAAC0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAojDvyCh9biYXrKKfypktoIDRtKjxbZduuSFCMIcHso1YlvnQ+dU11C+JRQvSb2J8VAfHjRjZFLmiBdVymUH3wdHKe5q0=', - existingExtensions: EXISTING_EXTENSIONS, - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 6612n, cells: 36n }, - timeDelta: 4662n // utime(1766042822) - last_paid(1766038160) - }, - - expected: { - // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork ? 75 : 0) - // cellLoads = 4, siblingIsFork = false - // gas = 5290 + 2400 + 0 = 7690 - gasUsed: 7690n, - gasFee: 3_076_000n, // 7690 × 400 - actionFee: 0n, - storageFee: 1751n, // ceil((6612×1 + 36×500) × 4662 / 65536) - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_884_551n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts deleted file mode 100644 index 99d6ef81a..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-remove-ext-prelast.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * V5R1 Remove Prelast Extension (2→1) - * https://tonviewer.com/transaction/4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47 - * - * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 - * Extension to remove: 0:0000000000000000000000000000000000000000000000000000000000000001 - * seqno: 3 - * utime: 1766048641 - * - * Purpose: Test remove extension gas when root fork collapses (2→1). - * - * REMOVE gas formula (from TVM dict_delete analysis): - * gas = 5290 + 600×cellLoads + (needsMerge ? 75 : 0) - * - * Where needsMerge = siblingIsFork OR rootCollapse (2→1) - * The +75 = 3 × cell_reload (3 × 25) for edge merge operations. - * - * With 2 extensions (ROOT FORK → 2 LEAFs): - * - cellLoads = 2 (root fork + target leaf) - * - siblingIsFork = false (sibling is LEAF) - * - rootCollapse = true (2→1 promotes remaining leaf to root) - * - gas = 5290 + 1200 + 75 = 6565 ✓ - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -// Existing 2 extensions (before removal) -const EXISTING_EXTENSIONS = [ - 'e8af660480cb56a355e06cfcd22cf474252d721ecbcb32bd98fcc8522b6ebbca', // #1 (first added) - '0000000000000000000000000000000000000000000000000000000000000001' // #2 - to be removed -]; - -export const V5R1_REMOVE_EXT_PRELAST: WalletFeeTestCase = { - txHash: '4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47', - - input: { - inMsgBoc: - 'te6cckEBAgEAmwABRYgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYMAQDlc2lnbn///xFpQ8mYAAAAA0DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM+ox14QmGm2ktksbaghTgG/OlpdAlQgbv1KoNQhwELXWy9RHsOIlCLmoVzoqKn7ElDbj0CMk4b2t9mGH0uqvgNDTc/mk=', - existingExtensions: EXISTING_EXTENSIONS, - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5546n, cells: 25n }, - timeDelta: 14042n // utime(1766048641) - last_paid(1766034599) - }, - - expected: { - // Formula: gas = 5290 + 600×cellLoads + (siblingIsFork || rootCollapse ? 75 : 0) - // cellLoads = 2 (root fork + target leaf) - // siblingIsFork = false, rootCollapse = true (2→1) - // +75 gas for edge merge (3 × cell_reload = 3 × 25) - // gas = 5290 + 1200 + 75 = 6565 - gasUsed: 6565n, - gasFee: 2_626_000n, // 6565 × 400 - actionFee: 0n, - storageFee: 3867n, // (5546×1 + 25×500) × 14042 / 65536 - importFee: 806_800n, - fwdFeeRemaining: 0n, - walletFee: 3_436_667n // 2_626_000 + 3867 + 806_800 - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts deleted file mode 100644 index 514d09b0a..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-send-all-transfer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * V5R1 Send All Balance Transfer - * https://tonviewer.com/transaction/9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f - * - * Wallet: UQBNUQQgFaC_XgIvEY-OcH_M5bMzrmlgFEBwHyI1fxVW-_d4 - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: ALL (entire balance ~0.37 TON) - * seqno: 5 - * Send mode: 130 (CARRY_ALL_REMAINING_BALANCE + IGNORE_ERRORS) - * - * Purpose: Test fee estimation for send-all mode. - * Verifies that sendMode doesn't affect gas calculation. - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_SEND_ALL_TRANSFER: WalletFeeTestCase = { - txHash: '9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f', - - input: { - inMsgBoc: - 'te6cckEBBAEAzAAB5YgAmqIIQCtBfrwEXiMfHOD/mctmZ1zSwCiA4D5Eav4qrfYDm0s7c///+ItKHuwwAAAALfm0UY5F646ThNTsVr5U1IpRiC2u40xbyGULBnenEi1OTCLRUcM9pCL9qzGl3Rwlm9hyB2RG+YNERLvVrRjSsg0BAgoOw8htggIDAAAAkmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0YehIAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNlbmQtYWxsIHRlc3Rw/eVR', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 3488n // utime(1766055009) - last_paid(1766051521) - }, - - expected: { - gasUsed: 4939n, - gasFee: 1_975_600n, - actionFee: 133_331n, - storageFee: 853n, - importFee: 769_600n, - fwdFeeRemaining: 266_669n, - walletFee: 3_146_053n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts deleted file mode 100644 index 177dbe802..000000000 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/v5r1-simple-transfer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * V5R1 Simple TON Transfer - * https://tonviewer.com/transaction/8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056 - * - * Wallet: EQDor2YEgMtWo1XgbPzSLPR0JS1yHsvLMr2Y_MhSK267ygG2 - * Destination: UQApwowlR6X54bXoso6orKCzCNm9ily8pAFy5vTwmsQ2WvVi - * Value: 0.01 TON - * seqno: 1 (NOT deploy - no StateInit) - * Send mode: 3 (PAY_GAS_SEPARATELY + IGNORE_ERRORS) - */ - -import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; -import { WalletFeeTestCase } from './utils'; -import { TonWalletVersion } from '../../compat'; - -export const V5R1_SIMPLE_TRANSFER: WalletFeeTestCase = { - txHash: '8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056', - - input: { - inMsgBoc: - 'te6cckEBBAEAygAB5YgB0V7MCQGWrUarwNn5pFno6Epa5D2XlmV7MfmQpFbdd5QDm0s7c///+ItKEsWAAAAADVL1hvMF+kXoAPfh8o5Si/90ln+7aUZX1+q49NXnnIx02PxrF0pgHtgV5DmXSIUI8apByap2eB+AIbWIi4vuGBEBAgoOw8htAwIDAAAAjmIAFOFGEqPS/PDa9FlHVFZQWYRs3sUuXlIAuXN6eE1iGy0cxLQAAAAAAAAAAAAAAAAAAAAAAABWNVIxIHNpbXBsZSB0ZXN0WOFYaQ==', - walletVersion: TonWalletVersion.V5R1, - storageUsed: { bits: 5012n, cells: 22n }, - timeDelta: 54358n // 1765955461 (utime) - 1765901103 (last_paid) - }, - - expected: { - gasUsed: 4939n, - gasFee: 1_975_600n, - actionFee: 133_331n, - storageFee: 13_281n, - importFee: 763_200n, - fwdFeeRemaining: 266_669n, - walletFee: 3_152_081n - }, - - blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 -}; From 02bbc242cee5b319a034f6ecdb1eae1040c08fbf Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 19:21:55 +0400 Subject: [PATCH 3/8] refactor(core): reorganize fee tests and add tonviewer links --- .../ton-blockchain/fee/__tests__/fees.spec.ts | 174 +++++++----------- .../fee/__tests__/fixtures/test-cases.ts | 26 +++ 2 files changed, 95 insertions(+), 105 deletions(-) diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts index 16924d6cd..41a33f0e1 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable prettier/prettier */ import { Cell } from '@ton/core'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -9,7 +10,7 @@ import { computeActionFee, computeAddExtensionGas, computeAddExtensionGasFromExtensions, - computeAddFirstExtensionGas, + computeForwardFee, computeGasFee, computeImportFee, @@ -332,124 +333,87 @@ describe('8. Blockchain-verified Transactions', () => { * All extension operations verified against real blockchain transactions. */ describe('7. V5R1 Extension Gas', () => { - describe('computeAddExtensionGasFromExtensions', () => { - // TX: 3eb607af0ee02aa773c9e840c817e62e2addc0871a6d6bdcd30e95784840a95e - it('0→1: empty dict → 6110', () => { - expect(computeAddExtensionGasFromExtensions([], EXT.E1)).toBe(6110n); - }); - - // TX: 185a5fd6fe0a996786b7acd4b2a5ff3b69df8475be91118d4ba726d90c4bc8f3 - it('1→2: 1 ext → 7210', () => { - expect(computeAddExtensionGasFromExtensions([EXT.E1], EXT.E2)).toBe(7210n); - }); - - // TX: d505f6df24065a837fe0e3916b4dffdadf4de45f20b784a6862c58b7609c9828 - it('2→3: 2 ext → 7810', () => { - expect(computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2], EXT.E3)).toBe(7810n); - }); - - // TX: e89c1640cd32335a123caa6737ac3767447f8818ff911bad03a3ba3555361565 - it('3→4: 3 ext → 8410', () => { - expect(computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2, EXT.E3], EXT.E4)).toBe( - 8410n - ); - }); + // ---- ADD Extension ---- + + // Full staircase 0→1→...→8 verified against real transactions. + // Gas is non-monotonic due to Patricia trie rebalancing (e.g. 4→5 < 3→4). + const addExtFromExtCases = [ + // https://tonviewer.com/transaction/3eb607af0ee02aa773c9e840c817e62e2addc0871a6d6bdcd30e95784840a95e + { existing: [], add: EXT.E1, gas: 6110n }, + // https://tonviewer.com/transaction/185a5fd6fe0a996786b7acd4b2a5ff3b69df8475be91118d4ba726d90c4bc8f3 + { existing: [EXT.E1], add: EXT.E2, gas: 7210n }, + // https://tonviewer.com/transaction/d505f6df24065a837fe0e3916b4dffdadf4de45f20b784a6862c58b7609c9828 + { existing: [EXT.E1, EXT.E2], add: EXT.E3, gas: 7810n }, + // https://tonviewer.com/transaction/e89c1640cd32335a123caa6737ac3767447f8818ff911bad03a3ba3555361565 + { existing: [EXT.E1, EXT.E2, EXT.E3], add: EXT.E4, gas: 8410n }, + // https://tonviewer.com/transaction/bdfdae4d4ddd87f0e45ee2249701e01538ec4d28a711e44b7debd2ba0c680f7b + { existing: [EXT.E1, EXT.E2, EXT.E3, EXT.E4], add: EXT.E5, gas: 7810n }, + // https://tonviewer.com/transaction/ca13fd5b2d0321128b265a7b4e1155ca142a08f8cc01523370385b05ab978e69 + { existing: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5], add: EXT.E6, gas: 8410n }, + // https://tonviewer.com/transaction/2e63cf4af8192d34f963656c632715ab66689a862d6f78e703360d3352adf07d + { existing: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6], add: EXT.E7, gas: 9010n }, + // https://tonviewer.com/transaction/a24db9110975efc27875b5786240384e96e58b29bf5497fefc84b8914f20a8a0 + { existing: [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7], add: EXT.E8, gas: 7810n } + ]; - // TX: bdfdae4d4ddd87f0e45ee2249701e01538ec4d28a711e44b7debd2ba0c680f7b - it('4→5: 4 ext → 7810', () => { - expect( - computeAddExtensionGasFromExtensions([EXT.E1, EXT.E2, EXT.E3, EXT.E4], EXT.E5) - ).toBe(7810n); - }); - - // TX: ca13fd5b2d0321128b265a7b4e1155ca142a08f8cc01523370385b05ab978e69 - it('5→6: 5 ext → 8410', () => { - expect( - computeAddExtensionGasFromExtensions( - [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5], - EXT.E6 - ) - ).toBe(8410n); - }); - - // TX: 2e63cf4af8192d34f963656c632715ab66689a862d6f78e703360d3352adf07d - it('6→7: 6 ext → 9010', () => { - expect( - computeAddExtensionGasFromExtensions( - [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6], - EXT.E7 - ) - ).toBe(9010n); - }); - - // TX: a24db9110975efc27875b5786240384e96e58b29bf5497fefc84b8914f20a8a0 - it('7→8: 7 ext → 7810', () => { - expect( - computeAddExtensionGasFromExtensions( - [EXT.E1, EXT.E2, EXT.E3, EXT.E4, EXT.E5, EXT.E6, EXT.E7], - EXT.E8 - ) - ).toBe(7810n); - }); - }); - - describe('computeAddFirstExtensionGas', () => { - it('returns 6110 for empty dict → 1 extension', () => { - expect(computeAddFirstExtensionGas()).toBe(6110n); - }); + describe('computeAddExtensionGasFromExtensions', () => { + for (const c of addExtFromExtCases) { + it(`${c.existing.length}→${c.existing.length + 1}: gas=${c.gas}`, () => { + expect(computeAddExtensionGasFromExtensions(c.existing, c.add)).toBe(c.gas); + }); + } }); - describe('computeAddExtensionGas (formula: 6610 + 600×cellLoads)', () => { - it('cellLoads=1 → 7210', () => { - expect(computeAddExtensionGas(1n)).toBe(7210n); - }); - - it('cellLoads=2 → 7810', () => { - expect(computeAddExtensionGas(2n)).toBe(7810n); - }); - - it('cellLoads=3 → 8410', () => { - expect(computeAddExtensionGas(3n)).toBe(8410n); - }); - - it('cellLoads=4 → 9010', () => { - expect(computeAddExtensionGas(4n)).toBe(9010n); - }); + // Formula: 6610 + 600 × cellLoads + const addExtCases = [ + { cellLoads: 1n, gas: 7210n }, + { cellLoads: 2n, gas: 7810n }, + { cellLoads: 3n, gas: 8410n }, + { cellLoads: 4n, gas: 9010n } + ]; + + describe('computeAddExtensionGas (6610 + 600×cellLoads)', () => { + for (const c of addExtCases) { + it(`cellLoads=${c.cellLoads} → ${c.gas}`, () => { + expect(computeAddExtensionGas(c.cellLoads)).toBe(c.gas); + }); + } }); // ---- REMOVE Extension ---- describe('computeRemoveLastExtensionGas', () => { - it('returns 5865 for 1→0 (5290 + 600 - 25)', () => { + it('1→0: 5290 + 600 - 25 = 5865', () => { expect(computeRemoveLastExtensionGas()).toBe(5865n); }); }); - describe('computeRemoveExtensionGas (formula: 5290 + 600×cellLoads ± merge)', () => { - it('cellLoads=1, no merge → 5890', () => { - expect(computeRemoveExtensionGas(1n, false)).toBe(5890n); - }); - - it('cellLoads=1, with merge (+75) → 5965', () => { - expect(computeRemoveExtensionGas(1n, true)).toBe(5965n); - }); - - it('cellLoads=2, no merge → 6490', () => { - expect(computeRemoveExtensionGas(2n, false)).toBe(6490n); - }); - - it('cellLoads=4, no merge → 7690', () => { - expect(computeRemoveExtensionGas(4n, false)).toBe(7690n); - }); + // Formula: 5290 + 600 × cellLoads + (needsMerge ? 75 : 0) + const removeExtCases = [ + { cellLoads: 1n, merge: false, gas: 5890n }, + { cellLoads: 1n, merge: true, gas: 5965n }, + { cellLoads: 2n, merge: false, gas: 6490n }, + { cellLoads: 4n, merge: false, gas: 7690n } + ]; + + describe('computeRemoveExtensionGas (5290 + 600×cellLoads ± merge)', () => { + for (const c of removeExtCases) { + it(`cellLoads=${c.cellLoads}, merge=${c.merge} → ${c.gas}`, () => { + expect(computeRemoveExtensionGas(c.cellLoads, c.merge)).toBe(c.gas); + }); + } }); - describe('computeRemoveExtensionGasFromExtensions', () => { - it('1→0: last extension → 5865', () => { - expect(computeRemoveExtensionGasFromExtensions([EXT.E1], EXT.E1)).toBe(5865n); - }); + const removeExtFromExtCases = [ + { existing: [EXT.E1], remove: EXT.E1, gas: 5865n, label: '1→0: last extension' }, + { existing: [EXT.E1, EXT.E2], remove: EXT.E2, gas: 6565n, label: '2→1: root collapse (+75)' } + ]; - it('2→1: root collapse (+75) → 6565', () => { - expect(computeRemoveExtensionGasFromExtensions([EXT.E1, EXT.E2], EXT.E2)).toBe(6565n); - }); + describe('computeRemoveExtensionGasFromExtensions', () => { + for (const c of removeExtFromExtCases) { + it(`${c.label} → ${c.gas}`, () => { + expect(computeRemoveExtensionGasFromExtensions(c.existing, c.remove)).toBe(c.gas); + }); + } }); }); diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts index 610af59de..a03ccfeaa 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts @@ -35,6 +35,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V3R1 // ============================================================ + // https://tonviewer.com/transaction/9b431557cc90d4fee34fe8b3afa5cc68baf0afac76d8a603f04bc6eccb0328a3 { name: 'V3R1 - Simple TON Transfer', tag: 'simple-transfer', @@ -61,6 +62,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/56d703d5a575c1ebebc1ca4c4d53a0fe153868f1819e90dce5454aaa60f85cbe { name: 'V3R1 - Deploy + Transfer', tag: 'deploy-transfer', @@ -87,6 +89,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/a4dc775cbbfc14c46679159a8e9fac6d65439e25fa68dcceb91c0e3de9948943 { name: 'V3R1 - Multi-message Transfer', tag: 'multi-transfer', @@ -117,6 +120,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V3R2 // ============================================================ + // https://tonviewer.com/transaction/2fa6487aaf22906418d98a8e20cb0c8fa1fb78c4d31661fb3ebc504ab5c9f9f7 { name: 'V3R2 - Simple TON Transfer', tag: 'simple-transfer', @@ -143,6 +147,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/a3c4513865506e14d8eb05e0c2e508827125d615384bbb1f91b19c2147088c99 { name: 'V3R2 - Deploy + Transfer', tag: 'deploy-transfer', @@ -169,6 +174,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/9758ce7b17d25f6f520d5c8be139de10abecd187fe16484263a0e5ae7fa1a298 { name: 'V3R2 - Multi-message Transfer', tag: 'multi-transfer', @@ -199,6 +205,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V4R2 // ============================================================ + // https://tonviewer.com/transaction/da87f551960c619ce4a00737d84c3ac087d311e30b3d0f0a481b6c528f639a11 { name: 'V4R2 - Simple TON Transfer', tag: 'simple-transfer', @@ -225,6 +232,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/9cef3b6ed79a0026f702997011dfae56ed1e542869be96433b2a2ee95e10dbc6 { name: 'V4R2 - Deploy + Transfer', tag: 'deploy-transfer', @@ -251,6 +259,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/f746a4a6347a56ad128bcd8e17831bbbd9776b0522c764e4c672512fa053196d { name: 'V4R2 - Multi-message Transfer', tag: 'multi-transfer', @@ -281,6 +290,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V5R1 // ============================================================ + // https://tonviewer.com/transaction/8612717faece81bf6a2c7b44c9b4609b71e06ae7d0c1aa356e6cd8f30c801056 { name: 'V5R1 - Simple TON Transfer', tag: 'simple-transfer', @@ -307,6 +317,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/8b399b6f07adfff9ebbc993f3e31955d28d01a0eca4e95927d221f793e01d5bb { name: 'V5R1 - Deploy + Transfer', tag: 'deploy-transfer', @@ -333,6 +344,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/9043311ef14e365b6a856a48d8126527363ef565c7aaa310948dcbfc691c526a { name: 'V5R1 - Multi-message Transfer', tag: 'multi-transfer', @@ -359,6 +371,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/9fc34b1f3bbea2afb2077224258c875b66fe468219d13eed32318aa3d72d2d2f { name: 'V5R1 - Send All Transfer', tag: 'send-all', @@ -389,6 +402,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V5R1 Jetton // ============================================================ + // https://tonviewer.com/transaction/4f148ce4f6ea7673dd7dce81e2f0cd23ca5e2e5baa68fa36ba0c689f324ce3ab { name: 'V5R1 - Deploy + Jetton Transfer', tag: 'jetton-deploy-transfer', @@ -415,6 +429,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/e1087deb86086b1e8496ab968f99a5530170ed631a534c4d8256329cf454dd70 { name: 'V5R1 - Simple Jetton Transfer', tag: 'jetton-transfer', @@ -445,6 +460,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V5R1 Dedup & Exotic // ============================================================ + // https://tonviewer.com/transaction/0339b0c0720456038ecfcaedc19f287341cd2df5ee0891321540070da554d054 { name: 'V5R1 - Dedup Within Msg', tag: 'dedup-within-msg', @@ -471,6 +487,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/139b5f7210fc0a86b54f447d0d060b1d843c1f52bf925057a7011461219a72c4 { name: 'V5R1 - Dedup Cross Msg', tag: 'dedup-cross-msg', @@ -497,6 +514,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/743d84f69adba2e65532d233a4ba93881bb6ffc1ea3fa476474fb4b49df32ec3 { name: 'V5R1 - Library Cell Body', tag: 'library-body', @@ -527,6 +545,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V5R1 Add Extension // ============================================================ + // https://tonviewer.com/transaction/0a1803894b487e63180e914013d3adcc227452c5ad9b646770bad745a8881f2a { name: 'V5R1 - Add First Extension', tag: 'add-extension', @@ -554,6 +573,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/30575e1ea9c73215b623c560562bf26fbd9ca5e32a4b35e3449b5763bba05c11 { name: 'V5R1 - Add Second Extension', tag: 'add-extension', @@ -581,6 +601,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/c4fec1044bce37f1969b8fc8fb4c25b52655439230d02d8bf70d6eee384ad729 { name: 'V5R1 - Add Eighth Extension', tag: 'add-extension', @@ -608,6 +629,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/6a16454aa6945d25787191caf686bf5df5bd2f9f581771ac1dc9adcc88315331 { name: 'V5R1 - Add Ninth Extension', tag: 'add-extension', @@ -639,6 +661,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ // V5R1 Remove Extension // ============================================================ + // https://tonviewer.com/transaction/7e06fd2ade80900e47bd38db060b9533d09bb9d28a8798fefc52457ec5d508e5 { name: 'V5R1 - Remove Ext (LEAF sibling)', tag: 'remove-extension', @@ -676,6 +699,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/e169d1236176c803883fe6bd2e5b8e482b51fafa428655fb7a47faafccbccb2b { name: 'V5R1 - Remove Ext (FORK sibling)', tag: 'remove-extension', @@ -703,6 +727,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/4862b0d85ded1db98571493e2d0af72cec7fd5e86fcb4b29c2cbe8ee690caf47 { name: 'V5R1 - Remove Ext (2→1)', tag: 'remove-extension', @@ -730,6 +755,7 @@ export const FEE_TEST_CASES: WalletFeeTestCase[] = [ blockchainConfig: BLOCKCHAIN_CONFIG_2024_12 }, + // https://tonviewer.com/transaction/9302d3bf88762bac10f62ea02e838eb6bd8f6c5330978143cc7edd24205d56e4 { name: 'V5R1 - Remove Ext (1→0)', tag: 'remove-extension', From f319930722012f318157e484bc211f1d27606736 Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 19:22:57 +0400 Subject: [PATCH 4/8] refactor(core): simplify fee tests and fix type duplication --- .../ton-blockchain/fee/__tests__/fees.spec.ts | 330 +++++++----------- .../fee/__tests__/fixtures/test-cases.ts | 7 +- .../fee/__tests__/fixtures/tonapi-fetcher.ts | 12 +- .../fee/__tests__/fixtures/utils.ts | 20 +- 4 files changed, 144 insertions(+), 225 deletions(-) diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts index 41a33f0e1..8c70eec94 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -3,14 +3,13 @@ import { Cell } from '@ton/core'; import { beforeAll, describe, expect, it } from 'vitest'; import { BLOCKCHAIN_CONFIG_2024_12 } from './fixtures/blockchain-config'; -import { fetchExpectedFees, shouldFetchRealFees, ExpectedFees } from './fixtures/tonapi-fetcher'; +import { fetchExpectedFees, shouldFetchRealFees } from './fixtures/tonapi-fetcher'; import { FEE_TEST_CASES, EXT } from './fixtures/test-cases'; -import { WalletFeeTestCase, parseWalletOutMsgCells } from './fixtures/utils'; +import { WalletFeeTestCase, ExpectedFees, parseWalletOutMsgCells } from './fixtures/utils'; import { computeActionFee, computeAddExtensionGas, computeAddExtensionGasFromExtensions, - computeForwardFee, computeGasFee, computeImportFee, @@ -21,9 +20,7 @@ import { computeWalletGasUsed, estimateWalletFee, EstimateWalletFeeParams, - extractFeeConfig, - parseV5R1ExtensionAction, - sumRefsStats + extractFeeConfig } from '../fees'; import { TonWalletVersion } from '../compat'; @@ -31,11 +28,12 @@ import { TonWalletVersion } from '../compat'; * TON Fee Calculation Specification * * This file serves as executable documentation for TON fee estimation. - * Each section documents a specific formula with unit tests. + * Sections 1-7: unit tests for individual formulas. + * Section 8: integration tests against real blockchain transactions. * * Modes: - * pnpm -F @tonkeeper/core test run fees.spec.ts # compare with fixtures - * FETCH_REAL_FEES=1 pnpm -F @tonkeeper/core test run fees.spec.ts # fetch from blockchain + * yarn workspace @tonkeeper/core exec vitest run fees.spec.ts # compare with fixtures + * FETCH_REAL_FEES=1 yarn workspace @tonkeeper/core exec vitest run fees.spec.ts # fetch from blockchain */ // Get basechain config for unit tests @@ -45,14 +43,22 @@ const unitTestConfig = extractFeeConfig(BLOCKCHAIN_CONFIG_2024_12, 0); // 1. computeGasFee // ============================================================================ -describe('1. computeGasFee (formula: gasUsed × gasPrice >> 16)', () => { +describe('1. computeGasFee (formula: floor(gasUsed × gasPrice / 2^16))', () => { it('returns 0 for gasUsed = 0', () => { expect(computeGasFee(unitTestConfig, 0n)).toBe(0n); }); - it('calculates gas fee: gasUsed=4939 → 1975600', () => { - // V5R1 simple transfer: 4939 gas units - expect(computeGasFee(unitTestConfig, 4939n)).toBe(1975600n); + it('calculates gas fee: gasUsed=4939 → 1_975_600', () => { + expect(computeGasFee(unitTestConfig, 4939n)).toBe(1_975_600n); + }); + + describe('floor rounding (gasPrice=1 → result = gasUsed / 2^16, truncated)', () => { + const config = { ...unitTestConfig, gasPrice: 1n }; + + it('rounds 0.0000... down to 0', () => expect(computeGasFee(config, 1n)).toBe(0n)); + it('rounds 0.9999... down to 0', () => expect(computeGasFee(config, 65535n)).toBe(0n)); + it('keeps exact 1.0 ', () => expect(computeGasFee(config, 65536n)).toBe(1n)); + it('rounds 1.0000... down to 1', () => expect(computeGasFee(config, 65537n)).toBe(1n)); }); }); @@ -60,30 +66,28 @@ describe('1. computeGasFee (formula: gasUsed × gasPrice >> 16)', () => { // 2. computeStorageFee // ============================================================================ -describe('2. computeStorageFee', () => { - /** - * Formula: ceil((bits × bitPrice + cells × cellPrice) × timeDelta / 2^16) - * Returns 0 when timeDelta <= 0 - */ +describe('2. computeStorageFee (formula: ceil((bits×bitPrice + cells×cellPrice) × timeDelta / 2^16))', () => { + it('returns 0 for timeDelta = 0', () => { + expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, 0n)).toBe(0n); + }); - describe('when timeDelta <= 0', () => { - it('returns 0 for timeDelta = 0', () => { - expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, 0n)).toBe(0n); - }); + it('returns 0 for negative timeDelta', () => { + expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, -100n)).toBe(0n); + }); - it('returns 0 for negative timeDelta', () => { - expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, -100n)).toBe(0n); - }); + it('calculates for V5R1 wallet (5012 bits, 22 cells, timeDelta=54358)', () => { + // used = 5012×1 + 22×500 = 16012 + // ceil(16012 × 54358 / 2^16) = ceil(870340696 / 65536) = 13281 + expect(computeStorageFee(unitTestConfig, { bits: 5012n, cells: 22n }, 54358n)).toBe(13281n); }); - describe('when timeDelta > 0', () => { - it('calculates for V5R1 wallet (5012 bits, 22 cells, timeDelta=54358)', () => { - // used = 5012×1 + 22×500 = 16012 - // ceil(16012 × 54358 / 2^16) = ceil(870340696 / 65536) = 13281 - expect(computeStorageFee(unitTestConfig, { bits: 5012n, cells: 22n }, 54358n)).toBe( - 13281n - ); - }); + // bitPrice=1, cellPrice=500 → used = 1×1 + 0×500 = 1, so result = timeDelta / 2^16 + describe('ceil rounding (used=1 → result = timeDelta / 2^16, rounded up)', () => { + const s = { bits: 1n, cells: 0n }; + + it('rounds 0.0000... up to 1', () => expect(computeStorageFee(unitTestConfig, s, 1n)).toBe(1n)); + it('keeps exact 1.0 ', () => expect(computeStorageFee(unitTestConfig, s, 65536n)).toBe(1n)); + it('rounds 1.0000... up to 2 ', () => expect(computeStorageFee(unitTestConfig, s, 65537n)).toBe(2n)); }); }); @@ -91,29 +95,35 @@ describe('2. computeStorageFee', () => { // 3. computeForwardFee // ============================================================================ -describe('3. computeForwardFee', () => { - /** - * Formula: lumpPrice + ceil((bitPrice × bits + cellPrice × cells) / 2^16) - * lumpPrice = 400000, bitPrice = 26214400, cellPrice = 2621440000 - */ +describe('3. computeForwardFee (formula: lumpPrice + ceil((bitPrice×bits + cellPrice×cells) / 2^16))', () => { + // lumpPrice = 400000, bitPrice = 26214400, cellPrice = 2621440000 it('returns lumpPrice for bits=0, cells=0', () => { - expect(computeForwardFee(unitTestConfig, 0n, 0n)).toBe(400000n); + expect(computeForwardFee(unitTestConfig, 0n, 0n)).toBe(400_000n); }); it('calculates for bits > 0, cells = 0', () => { // ceil(26214400 × 667 / 2^16) + 400000 = 266800 + 400000 = 666800 - expect(computeForwardFee(unitTestConfig, 667n, 0n)).toBe(666800n); + expect(computeForwardFee(unitTestConfig, 667n, 0n)).toBe(666_800n); }); it('calculates for bits = 0, cells > 0', () => { // ceil(2621440000 × 1 / 2^16) + 400000 = 40000 + 400000 = 440000 - expect(computeForwardFee(unitTestConfig, 0n, 1n)).toBe(440000n); + expect(computeForwardFee(unitTestConfig, 0n, 1n)).toBe(440_000n); }); it('calculates for bits > 0, cells > 0', () => { // ceil((26214400×667 + 2621440000×1) / 2^16) + 400000 = 306800 + 400000 = 706800 - expect(computeForwardFee(unitTestConfig, 667n, 1n)).toBe(706800n); + expect(computeForwardFee(unitTestConfig, 667n, 1n)).toBe(706_800n); + }); + + // lumpPrice=0, bitPrice=1, cellPrice=0 → result = ceil(bits / 2^16) + describe('ceil rounding (bitPrice=1, lump=0, cell=0 → result = bits / 2^16, rounded up)', () => { + const config = { ...unitTestConfig, msgFwdBitPrice: 1n, msgFwdCellPrice: 0n, msgFwdLumpPrice: 0n }; + + it('rounds 0.0000... up to 1', () => expect(computeForwardFee(config, 1n, 0n)).toBe(1n)); + it('keeps exact 1.0 ', () => expect(computeForwardFee(config, 65536n, 0n)).toBe(1n)); + it('rounds 1.0000... up to 2 ', () => expect(computeForwardFee(config, 65537n, 0n)).toBe(2n)); }); }); @@ -127,10 +137,8 @@ describe('4. computeImportFee', () => { * Used for external-in message import fee calculation. */ - it('uses same formula as computeForwardFee', () => { - expect(computeImportFee(unitTestConfig, 667n, 1n)).toBe( - computeForwardFee(unitTestConfig, 667n, 1n) - ); + it('calculates import fee: bits=667, cells=1 → 706800', () => { + expect(computeImportFee(unitTestConfig, 667n, 1n)).toBe(706800n); }); }); @@ -138,15 +146,27 @@ describe('4. computeImportFee', () => { // 5. computeActionFee // ============================================================================ -describe('5. computeActionFee (formula: fwdFee × firstFrac >> 16 ≈ 1/3)', () => { +describe('5. computeActionFee (formula: floor(fwdFee × firstFrac / 2^16) ≈ 1/3)', () => { + // firstFrac = 21845, so multiplier ≈ 21845/65536 ≈ 0.33333 + it('returns 0 for fwdFee = 0', () => { expect(computeActionFee(unitTestConfig, 0n)).toBe(0n); }); it('returns ~1/3 of forward fee', () => { - // fwdFee × firstFrac >> 16 = 666672 × 21845 >> 16 = 222220 + // 666672 × 21845 >> 16 = 222220 expect(computeActionFee(unitTestConfig, 666672n)).toBe(222220n); }); + + // firstFrac=1 → result = fwdFee / 2^16 + describe('floor rounding (firstFrac=1 → result = fwdFee / 2^16, truncated)', () => { + const config = { ...unitTestConfig, msgFwdFirstFrac: 1n }; + + it('rounds 0.0000... down to 0', () => expect(computeActionFee(config, 1n)).toBe(0n)); + it('rounds 0.9999... down to 0', () => expect(computeActionFee(config, 65535n)).toBe(0n)); + it('keeps exact 1.0 ', () => expect(computeActionFee(config, 65536n)).toBe(1n)); + it('rounds 1.0000... down to 1', () => expect(computeActionFee(config, 65537n)).toBe(1n)); + }); }); // ============================================================================ @@ -162,160 +182,19 @@ describe('6. computeWalletGasUsed (formula: baseGas + gasPerMsg × n)', () => { * | V3R2 | 2352 | 642 | * | V3R1 | 2275 | 642 | */ + const cases = [ + { version: TonWalletVersion.V5R1, label: 'V5R1', msgs: 1n, gas: 4939n }, // 4222 + 717×1 + { version: TonWalletVersion.V5R1, label: 'V5R1', msgs: 3n, gas: 6373n }, // 4222 + 717×3 + { version: TonWalletVersion.V4R2, label: 'V4R2', msgs: 1n, gas: 3308n }, // 2666 + 642×1 + { version: TonWalletVersion.V4R2, label: 'V4R2', msgs: 3n, gas: 4592n }, // 2666 + 642×3 + { version: TonWalletVersion.V3R2, label: 'V3R2', msgs: 1n, gas: 2994n }, // 2352 + 642×1 + { version: TonWalletVersion.V3R1, label: 'V3R1', msgs: 1n, gas: 2917n } // 2275 + 642×1 + ]; - describe('V5R1 (baseGas=4222, gasPerMsg=717)', () => { - it('1 msg: 4222 + 717×1 = 4939', () => { - expect(computeWalletGasUsed(TonWalletVersion.V5R1, 1n)).toBe(4939n); - }); - - it('3 msgs: 4222 + 717×3 = 6373', () => { - expect(computeWalletGasUsed(TonWalletVersion.V5R1, 3n)).toBe(6373n); - }); - }); - - describe('V4R2 (baseGas=2666, gasPerMsg=642)', () => { - it('1 msg: 2666 + 642×1 = 3308', () => { - expect(computeWalletGasUsed(TonWalletVersion.V4R2, 1n)).toBe(3308n); - }); - - it('3 msgs: 2666 + 642×3 = 4592', () => { - expect(computeWalletGasUsed(TonWalletVersion.V4R2, 3n)).toBe(4592n); - }); - }); - - describe('V3R2 (baseGas=2352, gasPerMsg=642)', () => { - it('1 msg: 2352 + 642×1 = 2994', () => { - expect(computeWalletGasUsed(TonWalletVersion.V3R2, 1n)).toBe(2994n); - }); - }); - - describe('V3R1 (baseGas=2275, gasPerMsg=642)', () => { - it('1 msg: 2275 + 642×1 = 2917', () => { - expect(computeWalletGasUsed(TonWalletVersion.V3R1, 1n)).toBe(2917n); - }); - }); -}); - -// ============================================================================ -// Helper functions for integration tests -// ============================================================================ - -async function loadExpected(fixture: WalletFeeTestCase): Promise { - if (shouldFetchRealFees()) { - console.log(`Fetching tx: ${fixture.txHash}`); - return fetchExpectedFees(fixture.txHash); - } - return fixture.expected; -} - -function createFeeTests(name: string, fixture: WalletFeeTestCase) { - describe(name, () => { - const { input, blockchainConfig } = fixture; - const storageUsed = input.storageUsed; - const timeDelta = input.timeDelta; - // Gas & storage use basechain config (wallet always in workchain 0) - const config = extractFeeConfig(blockchainConfig, 0); - // Convert base64 BOC to Cell - const inMsg = Cell.fromBase64(input.inMsgBoc); - // Extract outMsgs from inMsg (empty for extensions) - const outMsgs = parseWalletOutMsgCells(inMsg, input.walletVersion); - - // Extension test data (only set for extension fixtures) - const existingExtensions = input.existingExtensions; - const extensionAction = existingExtensions ? parseV5R1ExtensionAction(inMsg) : null; - const extensionHash = extensionAction?.address.hash.toString('hex') ?? ''; - const isRemoveExtension = extensionAction?.type === 'removeExtension'; - - let expected: ExpectedFees; - beforeAll(async () => { - expected = await loadExpected(fixture); - }); - - it('computeGasUsed', () => { - let gasUsed: bigint; - if (existingExtensions) { - gasUsed = isRemoveExtension - ? computeRemoveExtensionGasFromExtensions(existingExtensions, extensionHash) - : computeAddExtensionGasFromExtensions(existingExtensions, extensionHash); - } else { - gasUsed = computeWalletGasUsed(input.walletVersion, BigInt(outMsgs.length)); - } - expect(gasUsed).toBe(expected.gasUsed); - }); - - it('computeGasFee', () => { - let gasUsed: bigint; - if (existingExtensions) { - gasUsed = isRemoveExtension - ? computeRemoveExtensionGasFromExtensions(existingExtensions, extensionHash) - : computeAddExtensionGasFromExtensions(existingExtensions, extensionHash); - } else { - gasUsed = computeWalletGasUsed(input.walletVersion, BigInt(outMsgs.length)); - } - const gasFee = computeGasFee(config, gasUsed); - expect(gasFee).toBe(expected.gasFee); - }); - - it('computeActionFee', () => { - const actionFee = existingExtensions - ? 0n - : outMsgs.reduce((acc, msg) => { - const { bits, cells } = sumRefsStats(msg); - const fwdFee = computeForwardFee(config, bits, cells); - return acc + computeActionFee(config, fwdFee); - }, 0n); - expect(actionFee).toBe(expected.actionFee); - }); - - it('computeStorageFee', () => { - const storageFee = computeStorageFee(config, storageUsed, timeDelta); - expect(storageFee).toBe(expected.storageFee); - }); - - it('computeImportFee', () => { - const { bits, cells } = sumRefsStats(inMsg); - const importFee = computeImportFee(config, bits, cells); - expect(importFee).toBe(expected.importFee); - }); - - it('estimateWalletFee', () => { - const params: EstimateWalletFeeParams = existingExtensions - ? { - walletVersion: input.walletVersion as TonWalletVersion.V5R1, - storageUsed, - inMsg, - timeDelta, - existingExtensions - } - : { - walletVersion: input.walletVersion, - storageUsed, - inMsg, - timeDelta, - outMsgs - }; - const estimation = estimateWalletFee(blockchainConfig, params); - expect(estimation.gasFee).toBe(expected.gasFee); - expect(estimation.actionFee).toBe(expected.actionFee); - expect(estimation.importFee).toBe(expected.importFee); - expect(estimation.storageFee).toBe(expected.storageFee); - expect(estimation.fwdFeeRemaining).toBe(expected.fwdFeeRemaining); - expect(estimation.walletFee).toBe(expected.walletFee); + for (const c of cases) { + it(`${c.label}, ${c.msgs} msg → ${c.gas}`, () => { + expect(computeWalletGasUsed(c.version, c.msgs)).toBe(c.gas); }); - }); -} - -// ============================================================================ -// 8. Blockchain-verified Transactions -// ============================================================================ - -/** - * Integration tests with real blockchain transactions. - * Each fixture contains a real transaction hash and expected fee values. - */ -describe('8. Blockchain-verified Transactions', () => { - for (const fixture of FEE_TEST_CASES) { - createFeeTests(fixture.name, fixture); } }); @@ -417,3 +296,54 @@ describe('7. V5R1 Extension Gas', () => { } }); }); + + +// ============================================================================ +// 8. Blockchain-verified Transactions +// ============================================================================ + +/** + * Integration tests with real blockchain transactions. + * Each fixture contains a real transaction hash and expected fee values. + */ +describe('8. Blockchain-verified Transactions', () => { + async function loadExpected(fixture: WalletFeeTestCase): Promise { + if (shouldFetchRealFees()) { + console.log(`Fetching tx: ${fixture.txHash}`); + return fetchExpectedFees(fixture.txHash); + } + return fixture.expected; + } + + describe.each(FEE_TEST_CASES)('$name', (fixture) => { + const { input, blockchainConfig } = fixture; + const { walletVersion, storageUsed, timeDelta, existingExtensions } = input; + const inMsg = Cell.fromBase64(input.inMsgBoc); + const outMsgs = parseWalletOutMsgCells(inMsg, walletVersion); + + let expected: ExpectedFees; + beforeAll(async () => { + expected = await loadExpected(fixture); + }); + + it('walletFee', () => { + const params: EstimateWalletFeeParams = existingExtensions + ? { + walletVersion: walletVersion as TonWalletVersion.V5R1, + storageUsed, + inMsg, + timeDelta, + existingExtensions + } + : { + walletVersion, + storageUsed, + inMsg, + timeDelta, + outMsgs + }; + const estimation = estimateWalletFee(blockchainConfig, params); + expect(estimation.walletFee).toBe(expected.walletFee); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts index a03ccfeaa..352df1d1e 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/test-cases.ts @@ -2,12 +2,7 @@ * Consolidated TON fee calculation test fixtures. * * All test cases are real blockchain transactions verified against TonAPI. - * Generated by: npx tsx generate-test-cases.ts - * - * To add a new test case: - * 1. Create a fixture file, add to generate-test-cases.ts FIXTURES array - * 2. Run: npx tsx generate-test-cases.ts - * 3. Verify: yarn workspace @tonkeeper/core exec vitest run fees.spec.ts + * Verify: yarn workspace @tonkeeper/core exec vitest run fees.spec.ts */ import { BLOCKCHAIN_CONFIG_2024_12 } from './blockchain-config'; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts index 7cdfe1e0e..54951ed77 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts @@ -3,17 +3,9 @@ * Used in tests when FETCH_REAL_FEES=1 environment variable is set. */ -const TONAPI_BASE_URL = 'https://tonapi.io/v2'; +import { ExpectedFees } from './utils'; -export interface ExpectedFees { - gasUsed: bigint; - gasFee: bigint; - actionFee: bigint; - storageFee: bigint; - importFee: bigint; - fwdFeeRemaining: bigint; - walletFee: bigint; -} +const TONAPI_BASE_URL = 'https://tonapi.io/v2'; // tonapi transaction response types (partial) interface TonApiComputePhase { diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts index f76fc540a..25684c490 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts @@ -16,6 +16,16 @@ import { fileURLToPath } from 'url'; import { assertUnreachable, TonWalletVersion, FeeBlockchainConfig } from '../../compat'; import { CellStats } from '../../fees'; +export interface ExpectedFees { + gasUsed: bigint; + gasFee: bigint; + actionFee: bigint; + storageFee: bigint; + importFee: bigint; + fwdFeeRemaining: bigint; + walletFee: bigint; +} + export type FixtureTag = | 'simple-transfer' | 'deploy-transfer' @@ -40,15 +50,7 @@ export type WalletFeeTestCase = { timeDelta: bigint; existingExtensions?: string[]; // only for extension tests }; - expected: { - gasUsed: bigint; - gasFee: bigint; - actionFee: bigint; - storageFee: bigint; - importFee: bigint; - fwdFeeRemaining: bigint; - walletFee: bigint; - }; + expected: ExpectedFees; blockchainConfig: FeeBlockchainConfig; }; From 51de22b278c5052772165fc1d7e993f896623b3d Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 22:12:56 +0400 Subject: [PATCH 5/8] refactor(fees): per-workchain config types based on block.tlb --- .../ton-blockchain/fee/__tests__/fees.spec.ts | 68 ++++----- .../__tests__/fixtures/blockchain-config.ts | 55 ++++---- .../fee/__tests__/fixtures/utils.ts | 6 +- .../src/service/ton-blockchain/fee/compat.ts | 33 ----- .../src/service/ton-blockchain/fee/fees.ts | 131 +++++++++--------- .../src/service/ton-blockchain/fee/index.ts | 6 +- 6 files changed, 137 insertions(+), 162 deletions(-) diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts index 8c70eec94..c65d069f9 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -19,8 +19,7 @@ import { computeStorageFee, computeWalletGasUsed, estimateWalletFee, - EstimateWalletFeeParams, - extractFeeConfig + EstimateWalletFeeParams } from '../fees'; import { TonWalletVersion } from '../compat'; @@ -36,8 +35,8 @@ import { TonWalletVersion } from '../compat'; * FETCH_REAL_FEES=1 yarn workspace @tonkeeper/core exec vitest run fees.spec.ts # fetch from blockchain */ -// Get basechain config for unit tests -const unitTestConfig = extractFeeConfig(BLOCKCHAIN_CONFIG_2024_12, 0); +// Basechain config for unit tests +const baseConfig = BLOCKCHAIN_CONFIG_2024_12.basechain; // ============================================================================ // 1. computeGasFee @@ -45,20 +44,21 @@ const unitTestConfig = extractFeeConfig(BLOCKCHAIN_CONFIG_2024_12, 0); describe('1. computeGasFee (formula: floor(gasUsed × gasPrice / 2^16))', () => { it('returns 0 for gasUsed = 0', () => { - expect(computeGasFee(unitTestConfig, 0n)).toBe(0n); + expect(computeGasFee(baseConfig, 0n)).toBe(0n); }); it('calculates gas fee: gasUsed=4939 → 1_975_600', () => { - expect(computeGasFee(unitTestConfig, 4939n)).toBe(1_975_600n); + expect(computeGasFee(baseConfig, 4939n)).toBe(1_975_600n); }); describe('floor rounding (gasPrice=1 → result = gasUsed / 2^16, truncated)', () => { - const config = { ...unitTestConfig, gasPrice: 1n }; + // Override gasPrice to 1 so gasFee = gasUsed >> 16, isolating rounding behavior + const roundingConfig = { ...baseConfig, gasPrice: 1n }; - it('rounds 0.0000... down to 0', () => expect(computeGasFee(config, 1n)).toBe(0n)); - it('rounds 0.9999... down to 0', () => expect(computeGasFee(config, 65535n)).toBe(0n)); - it('keeps exact 1.0 ', () => expect(computeGasFee(config, 65536n)).toBe(1n)); - it('rounds 1.0000... down to 1', () => expect(computeGasFee(config, 65537n)).toBe(1n)); + it('rounds 0.0000... down to 0', () => expect(computeGasFee(roundingConfig, 1n)).toBe(0n)); + it('rounds 0.9999... down to 0', () => expect(computeGasFee(roundingConfig, 65535n)).toBe(0n)); + it('keeps exact 1.0 ', () => expect(computeGasFee(roundingConfig, 65536n)).toBe(1n)); + it('rounds 1.0000... down to 1', () => expect(computeGasFee(roundingConfig, 65537n)).toBe(1n)); }); }); @@ -68,26 +68,26 @@ describe('1. computeGasFee (formula: floor(gasUsed × gasPrice / 2^16))', () => describe('2. computeStorageFee (formula: ceil((bits×bitPrice + cells×cellPrice) × timeDelta / 2^16))', () => { it('returns 0 for timeDelta = 0', () => { - expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, 0n)).toBe(0n); + expect(computeStorageFee(baseConfig, { bits: 100n, cells: 1n }, 0n)).toBe(0n); }); it('returns 0 for negative timeDelta', () => { - expect(computeStorageFee(unitTestConfig, { bits: 100n, cells: 1n }, -100n)).toBe(0n); + expect(computeStorageFee(baseConfig, { bits: 100n, cells: 1n }, -100n)).toBe(0n); }); it('calculates for V5R1 wallet (5012 bits, 22 cells, timeDelta=54358)', () => { // used = 5012×1 + 22×500 = 16012 // ceil(16012 × 54358 / 2^16) = ceil(870340696 / 65536) = 13281 - expect(computeStorageFee(unitTestConfig, { bits: 5012n, cells: 22n }, 54358n)).toBe(13281n); + expect(computeStorageFee(baseConfig, { bits: 5012n, cells: 22n }, 54358n)).toBe(13281n); }); // bitPrice=1, cellPrice=500 → used = 1×1 + 0×500 = 1, so result = timeDelta / 2^16 describe('ceil rounding (used=1 → result = timeDelta / 2^16, rounded up)', () => { const s = { bits: 1n, cells: 0n }; - it('rounds 0.0000... up to 1', () => expect(computeStorageFee(unitTestConfig, s, 1n)).toBe(1n)); - it('keeps exact 1.0 ', () => expect(computeStorageFee(unitTestConfig, s, 65536n)).toBe(1n)); - it('rounds 1.0000... up to 2 ', () => expect(computeStorageFee(unitTestConfig, s, 65537n)).toBe(2n)); + it('rounds 0.0000... up to 1', () => expect(computeStorageFee(baseConfig, s, 1n)).toBe(1n)); + it('keeps exact 1.0 ', () => expect(computeStorageFee(baseConfig, s, 65536n)).toBe(1n)); + it('rounds 1.0000... up to 2 ', () => expect(computeStorageFee(baseConfig, s, 65537n)).toBe(2n)); }); }); @@ -99,31 +99,32 @@ describe('3. computeForwardFee (formula: lumpPrice + ceil((bitPrice×bits + cell // lumpPrice = 400000, bitPrice = 26214400, cellPrice = 2621440000 it('returns lumpPrice for bits=0, cells=0', () => { - expect(computeForwardFee(unitTestConfig, 0n, 0n)).toBe(400_000n); + expect(computeForwardFee(baseConfig.fwd, 0n, 0n)).toBe(400_000n); }); it('calculates for bits > 0, cells = 0', () => { // ceil(26214400 × 667 / 2^16) + 400000 = 266800 + 400000 = 666800 - expect(computeForwardFee(unitTestConfig, 667n, 0n)).toBe(666_800n); + expect(computeForwardFee(baseConfig.fwd, 667n, 0n)).toBe(666_800n); }); it('calculates for bits = 0, cells > 0', () => { // ceil(2621440000 × 1 / 2^16) + 400000 = 40000 + 400000 = 440000 - expect(computeForwardFee(unitTestConfig, 0n, 1n)).toBe(440_000n); + expect(computeForwardFee(baseConfig.fwd, 0n, 1n)).toBe(440_000n); }); it('calculates for bits > 0, cells > 0', () => { // ceil((26214400×667 + 2621440000×1) / 2^16) + 400000 = 306800 + 400000 = 706800 - expect(computeForwardFee(unitTestConfig, 667n, 1n)).toBe(706_800n); + expect(computeForwardFee(baseConfig.fwd, 667n, 1n)).toBe(706_800n); }); // lumpPrice=0, bitPrice=1, cellPrice=0 → result = ceil(bits / 2^16) describe('ceil rounding (bitPrice=1, lump=0, cell=0 → result = bits / 2^16, rounded up)', () => { - const config = { ...unitTestConfig, msgFwdBitPrice: 1n, msgFwdCellPrice: 0n, msgFwdLumpPrice: 0n }; + // Override prices so fwdFee = ceil(bits / 2^16), isolating rounding behavior + const roundingFwd = { ...baseConfig.fwd, bitPrice: 1n, cellPrice: 0n, lumpPrice: 0n }; - it('rounds 0.0000... up to 1', () => expect(computeForwardFee(config, 1n, 0n)).toBe(1n)); - it('keeps exact 1.0 ', () => expect(computeForwardFee(config, 65536n, 0n)).toBe(1n)); - it('rounds 1.0000... up to 2 ', () => expect(computeForwardFee(config, 65537n, 0n)).toBe(2n)); + it('rounds 0.0000... up to 1', () => expect(computeForwardFee(roundingFwd, 1n, 0n)).toBe(1n)); + it('keeps exact 1.0 ', () => expect(computeForwardFee(roundingFwd, 65536n, 0n)).toBe(1n)); + it('rounds 1.0000... up to 2 ', () => expect(computeForwardFee(roundingFwd, 65537n, 0n)).toBe(2n)); }); }); @@ -138,7 +139,7 @@ describe('4. computeImportFee', () => { */ it('calculates import fee: bits=667, cells=1 → 706800', () => { - expect(computeImportFee(unitTestConfig, 667n, 1n)).toBe(706800n); + expect(computeImportFee(baseConfig.fwd, 667n, 1n)).toBe(706800n); }); }); @@ -150,22 +151,23 @@ describe('5. computeActionFee (formula: floor(fwdFee × firstFrac / 2^16) ≈ 1/ // firstFrac = 21845, so multiplier ≈ 21845/65536 ≈ 0.33333 it('returns 0 for fwdFee = 0', () => { - expect(computeActionFee(unitTestConfig, 0n)).toBe(0n); + expect(computeActionFee(baseConfig.fwd, 0n)).toBe(0n); }); it('returns ~1/3 of forward fee', () => { // 666672 × 21845 >> 16 = 222220 - expect(computeActionFee(unitTestConfig, 666672n)).toBe(222220n); + expect(computeActionFee(baseConfig.fwd, 666672n)).toBe(222220n); }); // firstFrac=1 → result = fwdFee / 2^16 describe('floor rounding (firstFrac=1 → result = fwdFee / 2^16, truncated)', () => { - const config = { ...unitTestConfig, msgFwdFirstFrac: 1n }; + // Override firstFrac to 1 so actionFee = fwdFee >> 16, isolating rounding behavior + const roundingFwd = { ...baseConfig.fwd, firstFrac: 1n }; - it('rounds 0.0000... down to 0', () => expect(computeActionFee(config, 1n)).toBe(0n)); - it('rounds 0.9999... down to 0', () => expect(computeActionFee(config, 65535n)).toBe(0n)); - it('keeps exact 1.0 ', () => expect(computeActionFee(config, 65536n)).toBe(1n)); - it('rounds 1.0000... down to 1', () => expect(computeActionFee(config, 65537n)).toBe(1n)); + it('rounds 0.0000... down to 0', () => expect(computeActionFee(roundingFwd, 1n)).toBe(0n)); + it('rounds 0.9999... down to 0', () => expect(computeActionFee(roundingFwd, 65535n)).toBe(0n)); + it('keeps exact 1.0 ', () => expect(computeActionFee(roundingFwd, 65536n)).toBe(1n)); + it('rounds 1.0000... down to 1', () => expect(computeActionFee(roundingFwd, 65537n)).toBe(1n)); }); }); diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts index 1c6807ec4..eba61e45e 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/blockchain-config.ts @@ -1,34 +1,37 @@ +/* eslint-disable prettier/prettier */ /** - * TON blockchain configuration snapshots for testing. - * Structure matches BlockchainConfig from @ton-api/client. + * TON blockchain configuration snapshot for testing (mainnet, December 2024). * - * Config keys: - * - 18: storage_prices - * - 20/21: gas_limits_prices (masterchain/basechain) - * - 24/25: msg_forward_prices (masterchain/basechain) + * Values sourced from https://tonviewer.com/config + * TL-B definitions: https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb + * + * ConfigParam 18: StoragePrices (bit_price_ps / mc_bit_price_ps, cell_price_ps / mc_cell_price_ps) + * ConfigParam 20/21: GasLimitsPrices.gas_price (masterchain / basechain) + * ConfigParam 24/25: MsgForwardPrices (masterchain / basechain) */ -import { FeeBlockchainConfig } from '../../compat'; +import { FeeConfig } from '../../fees'; -export const BLOCKCHAIN_CONFIG_2024_12: FeeBlockchainConfig = { - '18': { - storagePrices: [ - { - bitPricePs: 1, - cellPricePs: 500 - } - ] - }, - '21': { - gasLimitsPrices: { - gasPrice: 26_214_400 +export const BLOCKCHAIN_CONFIG_2024_12: FeeConfig = { + basechain: { + gasPrice: 26_214_400n, // ConfigParam 21 + storageBitPrice: 1n, // ConfigParam 18: bit_price_ps + storageCellPrice: 500n, // ConfigParam 18: cell_price_ps + fwd: { // ConfigParam 25 + lumpPrice: 400_000n, + bitPrice: 26_214_400n, + cellPrice: 2_621_440_000n, + firstFrac: 21_845n } }, - '25': { - msgForwardPrices: { - lumpPrice: 400_000, - bitPrice: 26_214_400, - cellPrice: 2_621_440_000, - firstFrac: 21845 + masterchain: { + gasPrice: 655_360_000n, // ConfigParam 20 + storageBitPrice: 1_000n, // ConfigParam 18: mc_bit_price_ps + storageCellPrice: 500_000n, // ConfigParam 18: mc_cell_price_ps + fwd: { // ConfigParam 24 + lumpPrice: 10_000_000n, + bitPrice: 655_360_000n, + cellPrice: 65_536_000_000n, + firstFrac: 21_845n } } -} as const; +}; diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts index 25684c490..e5984a635 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/utils.ts @@ -13,8 +13,8 @@ import { readFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { assertUnreachable, TonWalletVersion, FeeBlockchainConfig } from '../../compat'; -import { CellStats } from '../../fees'; +import { assertUnreachable, TonWalletVersion } from '../../compat'; +import { CellStats, FeeConfig } from '../../fees'; export interface ExpectedFees { gasUsed: bigint; @@ -51,7 +51,7 @@ export type WalletFeeTestCase = { existingExtensions?: string[]; // only for extension tests }; expected: ExpectedFees; - blockchainConfig: FeeBlockchainConfig; + blockchainConfig: FeeConfig; }; export function normalizeHash(message: Message, normalizeExternal: boolean): Buffer { diff --git a/packages/core/src/service/ton-blockchain/fee/compat.ts b/packages/core/src/service/ton-blockchain/fee/compat.ts index 781d45e5f..00fd5a4bf 100644 --- a/packages/core/src/service/ton-blockchain/fee/compat.ts +++ b/packages/core/src/service/ton-blockchain/fee/compat.ts @@ -19,36 +19,3 @@ export enum TonWalletVersion { V4R2 = 'V4R2', V5R1 = 'V5R1' } - -/** - * Minimal blockchain config interface for fee calculation. - * Only the fields actually used by extractFeeConfig() are declared. - * Both @ton-api/client's BlockchainConfig and tonApiV2's BlockchainConfig - * satisfy this interface structurally. - */ -export interface FeeBlockchainConfig { - '18'?: { - storagePrices?: Array<{ - bitPricePs?: number; - cellPricePs?: number; - }>; - }; - '20'?: { gasLimitsPrices?: { gasPrice?: number } }; - '21'?: { gasLimitsPrices?: { gasPrice?: number } }; - '24'?: { - msgForwardPrices?: { - lumpPrice?: number; - bitPrice?: number; - cellPrice?: number; - firstFrac?: number; - }; - }; - '25'?: { - msgForwardPrices?: { - lumpPrice?: number; - bitPrice?: number; - cellPrice?: number; - firstFrac?: number; - }; - }; -} diff --git a/packages/core/src/service/ton-blockchain/fee/fees.ts b/packages/core/src/service/ton-blockchain/fee/fees.ts index 49fba9478..2a84b5495 100644 --- a/packages/core/src/service/ton-blockchain/fee/fees.ts +++ b/packages/core/src/service/ton-blockchain/fee/fees.ts @@ -1,6 +1,6 @@ import { Address, Cell } from '@ton/core'; -import { TonWalletVersion, FeeBlockchainConfig, assertUnreachable } from './compat'; +import { TonWalletVersion, assertUnreachable } from './compat'; // ============================================================================ // Types @@ -41,14 +41,45 @@ export interface WalletFeeEstimation { walletFee: bigint; } -export interface FeeConfigParams { - msgFwdBitPrice: bigint; // config 24/25 - msgFwdCellPrice: bigint; // config 24/25 - msgFwdLumpPrice: bigint; // config 24/25 - msgFwdFirstFrac: bigint; // config 24/25 - gasPrice: bigint; // config 20/21 - storageBitPrice: bigint; // config 18 - storageCellPrice: bigint; // config 18 +/** + * block.tlb: MsgForwardPrices#ea — message forwarding fee parameters. + * ConfigParam 24 (masterchain) / 25 (basechain). + * @see https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb + * @see https://docs.ton.org/v3/documentation/network/configs/blockchain-configs#param-24 + */ +export interface MsgForwardPrices { + /** lump_price:uint64 — base forwarding fee */ + lumpPrice: bigint; + /** bit_price:uint64 — per-bit forwarding fee */ + bitPrice: bigint; + /** cell_price:uint64 — per-cell forwarding fee */ + cellPrice: bigint; + /** first_frac:uint16 — fraction kept as action fee (out of 2^16) */ + firstFrac: bigint; +} + +/** + * Fee parameters for a single workchain. + * @see https://docs.ton.org/v3/documentation/network/configs/blockchain-configs + */ +export interface WorkchainConfig { + /** GasLimitsPrices.gas_price — nanotons per 2^16 gas units. + * ConfigParam 20 (masterchain) / 21 (basechain) */ + gasPrice: bigint; + /** StoragePrices.bit_price_ps — nanotons per bit per 2^16 seconds. + * ConfigParam 18: bit_price_ps (basechain) / mc_bit_price_ps (masterchain) */ + storageBitPrice: bigint; + /** StoragePrices.cell_price_ps — nanotons per cell per 2^16 seconds. + * ConfigParam 18: cell_price_ps (basechain) / mc_cell_price_ps (masterchain) */ + storageCellPrice: bigint; + /** MsgForwardPrices for this workchain */ + fwd: MsgForwardPrices; +} + +/** Complete fee configuration for both workchains. */ +export interface FeeConfig { + basechain: WorkchainConfig; + masterchain: WorkchainConfig; } // ============================================================================ @@ -87,62 +118,31 @@ export interface V4R2PluginAction { address: Address; } -// ============================================================================ -// Fee Config Extraction -// ============================================================================ - -// eslint-disable-next-line complexity -export function extractFeeConfig( - config: FeeBlockchainConfig, - workchain: WorkchainId = 0 -): FeeConfigParams { - const storageConfigKey = '18'; - const fwdConfigKey = workchain === -1 ? '24' : '25'; // masterchain / basechain - const gasConfigKey = workchain === -1 ? '20' : '21'; // masterchain / basechain - - const msgForwardPrices = config[fwdConfigKey]?.msgForwardPrices; - const gasPrices = config[gasConfigKey]?.gasLimitsPrices; - const storagePrices = config[storageConfigKey]?.storagePrices?.[0]; - - return { - msgFwdBitPrice: BigInt(msgForwardPrices?.bitPrice ?? 26214400), - msgFwdCellPrice: BigInt(msgForwardPrices?.cellPrice ?? 2621440000), - msgFwdLumpPrice: BigInt(msgForwardPrices?.lumpPrice ?? 400000), - msgFwdFirstFrac: BigInt(msgForwardPrices?.firstFrac ?? 21845), - gasPrice: BigInt(gasPrices?.gasPrice ?? 26214400), - storageBitPrice: BigInt(storagePrices?.bitPricePs ?? 1), - storageCellPrice: BigInt(storagePrices?.cellPricePs ?? 500) - }; -} - // ============================================================================ // Basic Fee Calculation Functions // ============================================================================ /** fwdFee = lumpPrice + ceil((bitPrice * bits + cellPrice * cells) / 2^16) */ -export function computeForwardFee(config: FeeConfigParams, bits: bigint, cells: bigint): bigint { - return ( - config.msgFwdLumpPrice + - shr16ceil(config.msgFwdBitPrice * bits + config.msgFwdCellPrice * cells) - ); +export function computeForwardFee(fwd: MsgForwardPrices, bits: bigint, cells: bigint): bigint { + return fwd.lumpPrice + shr16ceil(fwd.bitPrice * bits + fwd.cellPrice * cells); } /** Import fee uses same formula as forward fee */ export const computeImportFee = computeForwardFee; /** actionFee = floor(fwdFee * firstFrac / 2^16) — stays with sender, included in total_fees */ -export function computeActionFee(config: FeeConfigParams, fwdFee: bigint): bigint { - return (fwdFee * config.msgFwdFirstFrac) >> 16n; +export function computeActionFee(fwd: MsgForwardPrices, fwdFee: bigint): bigint { + return (fwdFee * fwd.firstFrac) >> 16n; } /** gasFee = floor(gasUsed * gasPrice / 2^16) */ -export function computeGasFee(config: FeeConfigParams, gasUsed: bigint): bigint { +export function computeGasFee(config: WorkchainConfig, gasUsed: bigint): bigint { return (gasUsed * config.gasPrice) >> 16n; } /** storageFee = ceil((bits * bitPrice + cells * cellPrice) * timeDelta / 2^16) */ export function computeStorageFee( - config: FeeConfigParams, + config: WorkchainConfig, storageUsed: CellStats, timeDelta: bigint ): bigint { @@ -237,7 +237,7 @@ function parseOutMsgDestWorkchain(outMsg: Cell): WorkchainId { * Compute action fee for outMsgs, checking destination workchain for each. * Forward prices differ for masterchain (-1) vs basechain (0). */ -function computeActionFeeForOutMsgs(config: FeeBlockchainConfig, outMsgs: Cell[]): bigint { +function computeActionFeeForOutMsgs(config: FeeConfig, outMsgs: Cell[]): bigint { return outMsgs.reduce((acc, msg) => { const destWorkchain = parseOutMsgDestWorkchain(msg); @@ -245,10 +245,10 @@ function computeActionFeeForOutMsgs(config: FeeBlockchainConfig, outMsgs: Cell[] console.warn('Destination workchain is masterchain, not tested yet!!!'); } - const fwdConfig = extractFeeConfig(config, destWorkchain); + const fwd = destWorkchain === -1 ? config.masterchain.fwd : config.basechain.fwd; const { bits, cells } = sumRefsStats(msg); - const fwdFee = computeForwardFee(fwdConfig, bits, cells); - return acc + computeActionFee(fwdConfig, fwdFee); + const fwdFee = computeForwardFee(fwd, bits, cells); + return acc + computeActionFee(fwd, fwdFee); }, 0n); } @@ -257,7 +257,7 @@ function computeActionFeeForOutMsgs(config: FeeBlockchainConfig, outMsgs: Cell[] * fwdFeeRemaining = fwdFee - actionFee ≈ 2/3 of forward fee. * This amount is deducted from the outMsg value during delivery. */ -export function computeFwdFeeRemaining(config: FeeBlockchainConfig, outMsgs: Cell[]): bigint { +export function computeFwdFeeRemaining(config: FeeConfig, outMsgs: Cell[]): bigint { return outMsgs.reduce((acc, msg) => { const destWorkchain = parseOutMsgDestWorkchain(msg); @@ -265,10 +265,10 @@ export function computeFwdFeeRemaining(config: FeeBlockchainConfig, outMsgs: Cel console.warn('Destination workchain is masterchain, not tested yet!!!'); } - const fwdConfig = extractFeeConfig(config, destWorkchain); + const fwd = destWorkchain === -1 ? config.masterchain.fwd : config.basechain.fwd; const { bits, cells } = sumRefsStats(msg); - const fwdFee = computeForwardFee(fwdConfig, bits, cells); - const actionFee = computeActionFee(fwdConfig, fwdFee); + const fwdFee = computeForwardFee(fwd, bits, cells); + const actionFee = computeActionFee(fwd, fwdFee); return acc + (fwdFee - actionFee); }, 0n); } @@ -363,6 +363,8 @@ export function parseV5R1ExtensionAction(inMsg: Cell): V5R1ExtensionAction | nul interface EstimateWalletFeeBaseParams { walletVersion: TonWalletVersion; + /** Workchain of the wallet. Defaults to 0 (basechain). */ + walletWorkchain?: WorkchainId; inMsg: Cell; timeDelta: bigint; /** @@ -412,23 +414,24 @@ export type EstimateWalletFeeParams = * @returns WalletFeeEstimation with all fee components including fwdFeeRemaining */ export function estimateWalletFee( - config: FeeBlockchainConfig, + config: FeeConfig, params: EstimateWalletFeeParams ): WalletFeeEstimation { const { walletVersion, inMsg, timeDelta, storageUsed } = params; + const walletWorkchain: WorkchainId = params.walletWorkchain ?? 0; - // Gas & storage fees use basechain config (wallet is always in workchain 0) - const baseConfig = extractFeeConfig(config, 0); + // Source workchain config (gas, storage, import fee) + const workchainConfig = walletWorkchain === -1 ? config.masterchain : config.basechain; // Common fees const { bits: msgBits, cells: msgCells } = sumRefsStats(inMsg); - const importFee = computeImportFee(baseConfig, msgBits, msgCells); - const storageFee = computeStorageFee(baseConfig, storageUsed, timeDelta); + const importFee = computeImportFee(workchainConfig.fwd, msgBits, msgCells); + const storageFee = computeStorageFee(workchainConfig, storageUsed, timeDelta); // === Transfer mode === if ('outMsgs' in params && params.outMsgs) { const gasUsed = computeWalletGasUsed(walletVersion, BigInt(params.outMsgs.length)); - const gasFee = computeGasFee(baseConfig, gasUsed); + const gasFee = computeGasFee(workchainConfig, gasUsed); // Action fee checks destination workchain for each outMsg const actionFee = computeActionFeeForOutMsgs(config, params.outMsgs); @@ -454,7 +457,7 @@ export function estimateWalletFee( params.existingExtensions, newExtensionHash ); - const gasFee = computeGasFee(baseConfig, gasUsed); + const gasFee = computeGasFee(workchainConfig, gasUsed); const actionFee = 0n; const fwdFeeRemaining = 0n; const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; @@ -467,7 +470,7 @@ export function estimateWalletFee( params.existingExtensions, removeExtensionHash ); - const gasFee = computeGasFee(baseConfig, gasUsed); + const gasFee = computeGasFee(workchainConfig, gasUsed); const actionFee = 0n; const fwdFeeRemaining = 0n; const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; @@ -492,7 +495,7 @@ export function estimateWalletFee( params.existingPlugins, newPluginHash ); - const gasFee = computeGasFee(baseConfig, gasUsed); + const gasFee = computeGasFee(workchainConfig, gasUsed); const actionFee = 0n; const fwdFeeRemaining = 0n; const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; @@ -505,7 +508,7 @@ export function estimateWalletFee( params.existingPlugins, removePluginHash ); - const gasFee = computeGasFee(baseConfig, gasUsed); + const gasFee = computeGasFee(workchainConfig, gasUsed); const actionFee = 0n; const fwdFeeRemaining = 0n; const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; diff --git a/packages/core/src/service/ton-blockchain/fee/index.ts b/packages/core/src/service/ton-blockchain/fee/index.ts index f560825ff..c7f59371a 100644 --- a/packages/core/src/service/ton-blockchain/fee/index.ts +++ b/packages/core/src/service/ton-blockchain/fee/index.ts @@ -1,7 +1,6 @@ export { UNINIT_ACCOUNT_STORAGE, estimateWalletFee, - extractFeeConfig, computeForwardFee, computeImportFee, computeActionFee, @@ -22,8 +21,10 @@ export { } from './fees'; export type { + MsgForwardPrices, + WorkchainConfig, + FeeConfig, WalletFeeEstimation, - FeeConfigParams, CellStats, WorkchainId, EstimateTransferFeeParams, @@ -35,4 +36,3 @@ export type { } from './fees'; export { TonWalletVersion } from './compat'; -export type { FeeBlockchainConfig } from './compat'; From ac4654b5f8d4a56adc7e33e179dea119e7ea2d7b Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 22:19:01 +0400 Subject: [PATCH 6/8] chore(eslint): add overrides for test files to allow devDependencies --- packages/core/.eslintrc.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index e331ffd24..d452feef4 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -1,3 +1,11 @@ module.exports = { - extends: ['../../.eslintrc.js'] + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['**/__tests__/**/*.ts', '**/*.spec.ts', '**/*.test.ts'], + rules: { + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }] + } + } + ] }; From 384d795c1040201503c4e6341225a5c639210745 Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 23:06:18 +0400 Subject: [PATCH 7/8] refactor(fees): reorganize file layout and minimize public API --- .../src/service/ton-blockchain/fee/fees.ts | 687 +++++++++--------- .../src/service/ton-blockchain/fee/index.ts | 22 +- 2 files changed, 351 insertions(+), 358 deletions(-) diff --git a/packages/core/src/service/ton-blockchain/fee/fees.ts b/packages/core/src/service/ton-blockchain/fee/fees.ts index 2a84b5495..bfedb2b60 100644 --- a/packages/core/src/service/ton-blockchain/fee/fees.ts +++ b/packages/core/src/service/ton-blockchain/fee/fees.ts @@ -1,3 +1,19 @@ +/** + * TON wallet fee estimation. + * + * Sections: + * 1. Types & Constants + * 2. estimateWalletFee (main entry point) + * 3. Basic fee formulas (gas, storage, forward, action) + * 4. Wallet gas parameters + * 5. Cell stats utilities + * 6. OutMsg processing + * 7. V5R1 message parser + * 8. Patricia trie internals + * 9. V5R1 extension gas (add / remove) + * 10. V4R2 plugin stubs + */ + import { Address, Cell } from '@ton/core'; import { TonWalletVersion, assertUnreachable } from './compat'; @@ -82,42 +98,223 @@ export interface FeeConfig { masterchain: WorkchainConfig; } +export interface V5R1ExtensionAction { + type: 'addExtension' | 'removeExtension'; + address: Address; +} + +/** V4R2 plugin action (for future implementation) */ +export interface V4R2PluginAction { + type: 'installPlugin' | 'removePlugin'; + address: Address; +} + +interface EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion; + /** Workchain of the wallet. Defaults to 0 (basechain). */ + walletWorkchain?: WorkchainId; + inMsg: Cell; + timeDelta: bigint; + /** + * Account storage stats (bits & cells) at the moment of the transaction. + * - For deploy (seqno=0, account not yet active): use {@link UNINIT_ACCOUNT_STORAGE} + * - For active wallets: get from `account.storage_stat.used` (e.g. via liteserver or TonAPI) + */ + storageUsed: CellStats; +} + +/** Transfer estimation params */ +export interface EstimateTransferFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion; + outMsgs: Cell[]; + existingExtensions?: never; + existingPlugins?: never; +} + +/** V5R1 Extension action estimation params */ +export interface EstimateExtensionFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion.V5R1; + outMsgs?: never; + existingExtensions: string[]; + existingPlugins?: never; +} + +/** V4R2 Plugin action estimation params */ +export interface EstimatePluginFeeParams extends EstimateWalletFeeBaseParams { + walletVersion: TonWalletVersion.V4R2; + outMsgs?: never; + existingExtensions?: never; + existingPlugins: string[]; +} + +export type EstimateWalletFeeParams = + | EstimateTransferFeeParams + | EstimateExtensionFeeParams + | EstimatePluginFeeParams; + +// ============================================================================ +// Main Entry Point +// ============================================================================ + +/** + * Estimate wallet transaction fee. + * + * For transfers: pass outMsgs array + * For V5R1 extension actions: pass existingExtensions (hex hashes from get_extensions) + * For V4R2 plugin actions: pass existingPlugins (hex hashes from get_plugins) + * + * @returns WalletFeeEstimation with all fee components including fwdFeeRemaining + */ +export function estimateWalletFee( + config: FeeConfig, + params: EstimateWalletFeeParams +): WalletFeeEstimation { + const { walletVersion, inMsg, timeDelta, storageUsed } = params; + const walletWorkchain: WorkchainId = params.walletWorkchain ?? 0; + + // Source workchain config (gas, storage, import fee) + const workchainConfig = walletWorkchain === -1 ? config.masterchain : config.basechain; + + // Common fees + const { bits: msgBits, cells: msgCells } = sumRefsStats(inMsg); + const importFee = computeImportFee(workchainConfig.fwd, msgBits, msgCells); + const storageFee = computeStorageFee(workchainConfig, storageUsed, timeDelta); + + // === Transfer mode === + if ('outMsgs' in params && params.outMsgs) { + const gasUsed = computeWalletGasUsed(walletVersion, BigInt(params.outMsgs.length)); + const gasFee = computeGasFee(workchainConfig, gasUsed); + + // Action fee checks destination workchain for each outMsg + const actionFee = computeActionFeeForOutMsgs(config, params.outMsgs); + + // fwdFeeRemaining = sum(fwdFee - actionFee) for all outMsgs + const fwdFeeRemaining = computeFwdFeeRemaining(config, params.outMsgs); + + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + // === V5R1 Extension mode === + if ('existingExtensions' in params && params.existingExtensions) { + const extensionAction = parseV5R1ExtensionAction(inMsg); + if (!extensionAction) { + throw new Error('Failed to parse extension action from inMsg'); + } + + if (extensionAction.type === 'addExtension') { + const newExtensionHash = extensionAction.address.hash.toString('hex'); + const gasUsed = computeAddExtensionGasFromExtensions( + params.existingExtensions, + newExtensionHash + ); + const gasFee = computeGasFee(workchainConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + if (extensionAction.type === 'removeExtension') { + const removeExtensionHash = extensionAction.address.hash.toString('hex'); + const gasUsed = computeRemoveExtensionGasFromExtensions( + params.existingExtensions, + removeExtensionHash + ); + const gasFee = computeGasFee(workchainConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + } + + // === V4R2 Plugin mode === + if ('existingPlugins' in params && params.existingPlugins) { + if (walletVersion !== TonWalletVersion.V4R2) { + throw new Error('Plugins are only supported for V4R2 wallets'); + } + + const pluginAction = parseV4R2PluginAction(inMsg); + if (!pluginAction) { + throw new Error('Failed to parse plugin action from inMsg'); + } + + if (pluginAction.type === 'installPlugin') { + const newPluginHash = pluginAction.address.hash.toString('hex'); + const gasUsed = computeInstallPluginGasFromPlugins( + params.existingPlugins, + newPluginHash + ); + const gasFee = computeGasFee(workchainConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + + if (pluginAction.type === 'removePlugin') { + const removePluginHash = pluginAction.address.hash.toString('hex'); + const gasUsed = computeRemovePluginGasFromPlugins( + params.existingPlugins, + removePluginHash + ); + const gasFee = computeGasFee(workchainConfig, gasUsed); + const actionFee = 0n; + const fwdFeeRemaining = 0n; + const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; + return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; + } + } + + throw new Error('Invalid params: provide outMsgs, existingExtensions, or existingPlugins'); +} + // ============================================================================ // Internal Constants // ============================================================================ +const ZERO_STATS: CellStats = { bits: 0n, cells: 0n }; + /** ceil(x / 2^16) */ const shr16ceil = (x: bigint): bigint => (x + 0xffffn) >> 16n; /** Gas cost for first cell read (transforming Cell → Slice) */ const TVM_CELL_READ_GAS = 100n; - /** Gas cost for cell write (Builder → Cell) */ const TVM_CELL_WRITE_GAS = 500n; - /** Gas cost for cell reload (re-reading already loaded cell) */ const TVM_CELL_RELOAD_GAS = 25n; -/** V5R1 extension action overhead (parsing action, checking existence, etc.) */ +/** + * V5R1 extension action overheads — empirically derived from emulation. + * + * EXTENSION_OVERHEAD / REMOVE_EXTENSION_OVERHEAD: + * Gas for parsing action tag, checking dict existence, etc. + * ADD is higher because it also creates fork + leaf cells. + * + * REMOVE_EDGE_MERGE_GAS: + * +75 gas (3 × cell_reload = 3 × 25) when edge merging occurs. + * In TVM cell_builder_add_slice_bool, when sibling is FORK: + * - size_refs() = 2 → prefetch_ref called twice + * Applies to two cases: + * - siblingIsFork: load FORK sibling + prefetch its 2 child refs + * - rootCollapse (2→1): merge remaining leaf with former root + * + * Verified against blockchain: + * - DELETE from 9 ext (sibling = LEAF): gas = 7690 (no merge) + * - DELETE from 8 ext (sibling = FORK): gas = 7765 (+75) + * - DELETE from 2 ext (root collapse): gas = 6565 (+75) + */ const V5R1_EXTENSION_OVERHEAD = 1388n; - -const ZERO_STATS: CellStats = { bits: 0n, cells: 0n }; +const V5R1_REMOVE_EXTENSION_OVERHEAD = 1068n; +const V5R1_REMOVE_EDGE_MERGE_GAS = TVM_CELL_RELOAD_GAS * 3n; /** V5R1 action tags */ const V5R1_ACTION_ADD_EXTENSION = 0x02; const V5R1_ACTION_REMOVE_EXTENSION = 0x03; -export interface V5R1ExtensionAction { - type: 'addExtension' | 'removeExtension'; - address: Address; -} - -/** V4R2 plugin action (for future implementation) */ -export interface V4R2PluginAction { - type: 'installPlugin' | 'removePlugin'; - address: Address; -} - // ============================================================================ // Basic Fee Calculation Functions // ============================================================================ @@ -156,7 +353,7 @@ export function computeStorageFee( // Wallet Gas Calculation // ============================================================================ -export function getWalletGasParams(version: TonWalletVersion): { +export function walletGasParams(version: TonWalletVersion): { baseGas: bigint; gasPerMsg: bigint; } { @@ -175,7 +372,7 @@ export function getWalletGasParams(version: TonWalletVersion): { } export function computeWalletGasUsed(version: TonWalletVersion, outMsgsCount: bigint): bigint { - const { baseGas, gasPerMsg } = getWalletGasParams(version); + const { baseGas, gasPerMsg } = walletGasParams(version); return baseGas + gasPerMsg * outMsgsCount; } @@ -242,7 +439,10 @@ function computeActionFeeForOutMsgs(config: FeeConfig, outMsgs: Cell[]): bigint const destWorkchain = parseOutMsgDestWorkchain(msg); if (destWorkchain === -1) { - console.warn('Destination workchain is masterchain, not tested yet!!!'); + console.warn( + 'Masterchain destination: fee calculation covers this path but lacks blockchain-verified tests. ' + + 'Please add masterchain transaction fixtures to fees.spec.ts' + ); } const fwd = destWorkchain === -1 ? config.masterchain.fwd : config.basechain.fwd; @@ -262,7 +462,10 @@ export function computeFwdFeeRemaining(config: FeeConfig, outMsgs: Cell[]): bigi const destWorkchain = parseOutMsgDestWorkchain(msg); if (destWorkchain === -1) { - console.warn('Destination workchain is masterchain, not tested yet!!!'); + console.warn( + 'Masterchain destination: fee calculation covers this path but lacks blockchain-verified tests. ' + + 'Please add masterchain transaction fixtures to fees.spec.ts' + ); } const fwd = destWorkchain === -1 ? config.masterchain.fwd : config.basechain.fwd; @@ -358,229 +561,39 @@ export function parseV5R1ExtensionAction(inMsg: Cell): V5R1ExtensionAction | nul } // ============================================================================ -// Main Wallet Fee Estimator +// Patricia Trie for Extension Gas Calculation // ============================================================================ -interface EstimateWalletFeeBaseParams { - walletVersion: TonWalletVersion; - /** Workchain of the wallet. Defaults to 0 (basechain). */ - walletWorkchain?: WorkchainId; - inMsg: Cell; - timeDelta: bigint; - /** - * Account storage stats (bits & cells) at the moment of the transaction. - * - For deploy (seqno=0, account not yet active): use {@link UNINIT_ACCOUNT_STORAGE} - * - For active wallets: get from `account.storage_stat.used` (e.g. via liteserver or TonAPI) - */ - storageUsed: CellStats; +interface LeafNode { + type: 'leaf'; + key: string; } -/** Transfer estimation params */ -export interface EstimateTransferFeeParams extends EstimateWalletFeeBaseParams { - walletVersion: TonWalletVersion; - outMsgs: Cell[]; - existingExtensions?: never; - existingPlugins?: never; +interface ForkNode { + type: 'fork'; + labelLength: number; + left?: TrieNode; + right?: TrieNode; } -/** V5R1 Extension action estimation params */ -export interface EstimateExtensionFeeParams extends EstimateWalletFeeBaseParams { - walletVersion: TonWalletVersion.V5R1; - outMsgs?: never; - existingExtensions: string[]; - existingPlugins?: never; +type TrieNode = LeafNode | ForkNode; + +function hexToBinary(hex: string): string { + return hex + .split('') + .map(c => parseInt(c, 16).toString(2).padStart(4, '0')) + .join(''); } -/** V4R2 Plugin action estimation params */ -export interface EstimatePluginFeeParams extends EstimateWalletFeeBaseParams { - walletVersion: TonWalletVersion.V4R2; - outMsgs?: never; - existingExtensions?: never; - existingPlugins: string[]; +function commonPrefixLength(a: string, b: string): number { + let i = 0; + while (i < a.length && i < b.length && a[i] === b[i]) i++; + return i; } -export type EstimateWalletFeeParams = - | EstimateTransferFeeParams - | EstimateExtensionFeeParams - | EstimatePluginFeeParams; - -/** - * Estimate wallet transaction fee. - * - * For transfers: pass outMsgs array - * For V5R1 extension actions: pass existingExtensions (hex hashes from get_extensions) - * For V4R2 plugin actions: pass existingPlugins (hex hashes from get_plugins) - * - * @returns WalletFeeEstimation with all fee components including fwdFeeRemaining - */ -export function estimateWalletFee( - config: FeeConfig, - params: EstimateWalletFeeParams -): WalletFeeEstimation { - const { walletVersion, inMsg, timeDelta, storageUsed } = params; - const walletWorkchain: WorkchainId = params.walletWorkchain ?? 0; - - // Source workchain config (gas, storage, import fee) - const workchainConfig = walletWorkchain === -1 ? config.masterchain : config.basechain; - - // Common fees - const { bits: msgBits, cells: msgCells } = sumRefsStats(inMsg); - const importFee = computeImportFee(workchainConfig.fwd, msgBits, msgCells); - const storageFee = computeStorageFee(workchainConfig, storageUsed, timeDelta); - - // === Transfer mode === - if ('outMsgs' in params && params.outMsgs) { - const gasUsed = computeWalletGasUsed(walletVersion, BigInt(params.outMsgs.length)); - const gasFee = computeGasFee(workchainConfig, gasUsed); - - // Action fee checks destination workchain for each outMsg - const actionFee = computeActionFeeForOutMsgs(config, params.outMsgs); - - // fwdFeeRemaining = sum(fwdFee - actionFee) for all outMsgs - const fwdFeeRemaining = computeFwdFeeRemaining(config, params.outMsgs); - - const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; - - return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; - } - - // === V5R1 Extension mode === - if ('existingExtensions' in params && params.existingExtensions) { - const extensionAction = parseV5R1ExtensionAction(inMsg); - if (!extensionAction) { - throw new Error('Failed to parse extension action from inMsg'); - } - - if (extensionAction.type === 'addExtension') { - const newExtensionHash = extensionAction.address.hash.toString('hex'); - const gasUsed = computeAddExtensionGasFromExtensions( - params.existingExtensions, - newExtensionHash - ); - const gasFee = computeGasFee(workchainConfig, gasUsed); - const actionFee = 0n; - const fwdFeeRemaining = 0n; - const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; - return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; - } - - if (extensionAction.type === 'removeExtension') { - const removeExtensionHash = extensionAction.address.hash.toString('hex'); - const gasUsed = computeRemoveExtensionGasFromExtensions( - params.existingExtensions, - removeExtensionHash - ); - const gasFee = computeGasFee(workchainConfig, gasUsed); - const actionFee = 0n; - const fwdFeeRemaining = 0n; - const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; - return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; - } - } - - // === V4R2 Plugin mode === - if ('existingPlugins' in params && params.existingPlugins) { - if (walletVersion !== TonWalletVersion.V4R2) { - throw new Error('Plugins are only supported for V4R2 wallets'); - } - - const pluginAction = parseV4R2PluginAction(inMsg); - if (!pluginAction) { - throw new Error('Failed to parse plugin action from inMsg'); - } - - if (pluginAction.type === 'installPlugin') { - const newPluginHash = pluginAction.address.hash.toString('hex'); - const gasUsed = computeInstallPluginGasFromPlugins( - params.existingPlugins, - newPluginHash - ); - const gasFee = computeGasFee(workchainConfig, gasUsed); - const actionFee = 0n; - const fwdFeeRemaining = 0n; - const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; - return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; - } - - if (pluginAction.type === 'removePlugin') { - const removePluginHash = pluginAction.address.hash.toString('hex'); - const gasUsed = computeRemovePluginGasFromPlugins( - params.existingPlugins, - removePluginHash - ); - const gasFee = computeGasFee(workchainConfig, gasUsed); - const actionFee = 0n; - const fwdFeeRemaining = 0n; - const walletFee = gasFee + actionFee + importFee + storageFee + fwdFeeRemaining; - return { gasFee, actionFee, importFee, storageFee, fwdFeeRemaining, walletFee }; - } - } - - throw new Error('Invalid params: provide outMsgs, existingExtensions, or existingPlugins'); -} - -// ============================================================================ -// V5R1 Extension Gas Calculation -// ============================================================================ - -/** - * Compute gas for V5R1 AddExtension action. - * - * Based on TVM dict_set implementation (crypto/vm/dict.cpp): - * - Each cell traversed during insertion costs 100 gas (cell load) - * - Each cell created costs 500 gas (cell create) - * - For new key insertion: cellCreates = cellLoads + 2 - * - * Formula: - * gas = baseGas + overhead + cellLoads×100 + (cellLoads+2)×500 - * = 5610 + 100×L + 500×L + 1000 - * = 6610 + 600×cellLoads - * - * @param cellLoads - number of cells traversed from root to insertion point - */ -export function computeAddExtensionGas(cellLoads: bigint): bigint { - const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); - // baseGas(4222) + overhead(1388) + cellLoads×100 + (cellLoads+2)×500 - // = 5610 + 100×L + 500×L + 1000 = 6610 + 600×L - return ( - baseGas + - V5R1_EXTENSION_OVERHEAD + - 1000n + - cellLoads * (TVM_CELL_READ_GAS + TVM_CELL_WRITE_GAS) - ); -} - -/** - * Compute gas for adding first extension (empty dict → 1 extension). - * Special case: no trie traversal, just create one cell. - * Returns 6110 gas units. - */ -export function computeAddFirstExtensionGas(): bigint { - const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); - return baseGas + V5R1_EXTENSION_OVERHEAD + TVM_CELL_WRITE_GAS; -} - -// ============================================================================ -// Patricia Trie for Extension Gas Calculation -// ============================================================================ - -interface TrieNode { - type: 'leaf' | 'fork'; - key?: string; - labelLength?: number; - left?: TrieNode; - right?: TrieNode; -} - -function commonPrefixLength(a: string, b: string): number { - let i = 0; - while (i < a.length && i < b.length && a[i] === b[i]) i++; - return i; -} - -function buildTrie(keys: string[]): TrieNode | null { - if (keys.length === 0) return null; - if (keys.length === 1) return { type: 'leaf', key: keys[0] }; +function buildTrie(keys: string[]): TrieNode | null { + if (keys.length === 0) return null; + if (keys.length === 1) return { type: 'leaf', key: keys[0] }; let prefix = keys[0]; for (let i = 1; i < keys.length; i++) { @@ -600,7 +613,7 @@ function buildTrie(keys: string[]): TrieNode | null { function getAllKeys(node: TrieNode | undefined): string[] { if (!node) return []; - if (node.type === 'leaf') return [node.key!]; + if (node.type === 'leaf') return [node.key]; return [...getAllKeys(node.left), ...getAllKeys(node.right)]; } @@ -627,7 +640,7 @@ function countCellLoads(node: TrieNode | undefined, key: string, pos = 0): numbe // Fork node - check if label matches const nodeKeys = getAllKeys(node); - const labelBits = node.labelLength! - pos; // bits in this node's label + const labelBits = node.labelLength - pos; // bits in this node's label const nodePrefix = nodeKeys[0].slice(pos, node.labelLength); const keySlice = key.slice(pos, node.labelLength); @@ -641,10 +654,108 @@ function countCellLoads(node: TrieNode | undefined, key: string, pos = 0): numbe } // Label matches (or pure fork with labelBits=0) - continue to child - const nextBit = key[node.labelLength!]; + const nextBit = key[node.labelLength]; const child = nextBit === '0' ? node.left : node.right; - return loads + countCellLoads(child, key, node.labelLength! + 1); + return loads + countCellLoads(child, key, node.labelLength + 1); +} + +/** + * Find sibling node after removing a key from trie. + * Returns { cellLoads, siblingIsFork } or null if key not found. + * + * The sibling is the other branch of the parent fork after deletion. + * When we delete a leaf, its parent fork merges with the sibling. + */ +function findDeleteInfo( + node: TrieNode | undefined, + key: string, + pos = 0 +): { cellLoads: number; siblingIsFork: boolean } | null { + if (!node) return null; + + // Count cell load for this node + const loads = 1; + + if (node.type === 'leaf') { + // Found the leaf to delete - but no sibling info here + // (sibling is determined by parent, which we track below) + return node.key === key ? { cellLoads: loads, siblingIsFork: false } : null; + } + + // Fork node - check if label matches + const nodeKeys = getAllKeys(node); + const labelBits = node.labelLength - pos; + const nodePrefix = nodeKeys[0].slice(pos, node.labelLength); + const keySlice = key.slice(pos, node.labelLength); + + // Check common prefix length + let pfxLen = 0; + while (pfxLen < labelBits && nodePrefix[pfxLen] === keySlice[pfxLen]) pfxLen++; + + if (pfxLen < labelBits) { + // Mismatch - key not in trie + return null; + } + + // Label matches - continue to child + const nextBit = key[node.labelLength]; + const child = nextBit === '0' ? node.left : node.right; + const sibling = nextBit === '0' ? node.right : node.left; + + const childResult = findDeleteInfo(child, key, node.labelLength + 1); + if (!childResult) return null; + + // If child is the leaf being deleted, determine sibling type + if (child?.type === 'leaf' && child.key === key) { + // Sibling will be merged with parent fork + const siblingIsFork = sibling?.type === 'fork'; + return { cellLoads: loads + childResult.cellLoads, siblingIsFork }; + } + + // Propagate from deeper in the tree + return { cellLoads: loads + childResult.cellLoads, siblingIsFork: childResult.siblingIsFork }; +} + +// ============================================================================ +// V5R1 Extension Gas Calculation: Add +// ============================================================================ + +/** + * Compute gas for V5R1 AddExtension action. + * + * Based on TVM dict_set implementation (crypto/vm/dict.cpp): + * - Each cell traversed during insertion costs 100 gas (cell load) + * - Each cell created costs 500 gas (cell create) + * - For new key insertion: cellCreates = cellLoads + 2 + * + * Formula: + * gas = baseGas + overhead + cellLoads×100 + (cellLoads+2)×500 + * = 5610 + 100×L + 500×L + 1000 + * = 6610 + 600×cellLoads + * + * @param cellLoads - number of cells traversed from root to insertion point + */ +export function computeAddExtensionGas(cellLoads: bigint): bigint { + const { baseGas } = walletGasParams(TonWalletVersion.V5R1); + // baseGas(4222) + overhead(1388) + cellLoads×100 + (cellLoads+2)×500 + // = 5610 + 100×L + 500×L + 1000 = 6610 + 600×L + return ( + baseGas + + V5R1_EXTENSION_OVERHEAD + + 1000n + + cellLoads * (TVM_CELL_READ_GAS + TVM_CELL_WRITE_GAS) + ); +} + +/** + * Compute gas for adding first extension (empty dict → 1 extension). + * Special case: no trie traversal, just create one cell. + * Returns 6110 gas units. + */ +export function computeAddFirstExtensionGas(): bigint { + const { baseGas } = walletGasParams(TonWalletVersion.V5R1); + return baseGas + V5R1_EXTENSION_OVERHEAD + TVM_CELL_WRITE_GAS; } /** @@ -669,13 +780,6 @@ export function computeAddExtensionGasFromExtensions( return computeAddFirstExtensionGas(); } - function hexToBinary(hex: string): string { - return hex - .split('') - .map(c => parseInt(c, 16).toString(2).padStart(4, '0')) - .join(''); - } - const binaryKeys = existingExtensionHashes.map(hexToBinary); const trie = buildTrie(binaryKeys); @@ -686,36 +790,9 @@ export function computeAddExtensionGasFromExtensions( } // ============================================================================ -// Remove Extension Gas Calculation +// V5R1 Extension Gas Calculation: Remove // ============================================================================ -/** - * V5R1 remove extension overhead (smaller than ADD because no new cells created). - * - * Empirically derived from emulation: - * - ADD overhead: 1388 + 1000 (fork + leaf creation) = 2388 - * - REMOVE overhead: 1068 (no new cells, may merge existing) - */ -const V5R1_REMOVE_EXTENSION_OVERHEAD = 1068n; - -/** - * Extra gas when merging with FORK sibling (has 2 child refs). - * - * In TVM cell_builder_add_slice_bool, when sibling is FORK: - * - size_refs() = 2 → prefetch_ref called twice - * - Each prefetch_ref may add ~37.5 gas overhead - * - * The +75 gas equals 3 × cell_reload (3 × 25 = 75) for edge merge operations: - * - siblingIsFork case: load FORK sibling + prefetch its 2 child refs - * - rootCollapse (2→1) case: merge remaining leaf with former root - * - * Verified against blockchain: - * - DELETE from 9 ext (sibling = LEAF): gas = 7690 (no merge) - * - DELETE from 8 ext (sibling = FORK): gas = 7765 (+75 for FORK handling) - * - DELETE from 2 ext (root collapse): gas = 6565 (+75 for root merge) - */ -const V5R1_REMOVE_EDGE_MERGE_GAS = TVM_CELL_RELOAD_GAS * 3n; - /** * Compute gas for V5R1 RemoveExtension action. * @@ -743,7 +820,7 @@ const V5R1_REMOVE_EDGE_MERGE_GAS = TVM_CELL_RELOAD_GAS * 3n; * @param needsMergeGas - true if edge merge required (siblingIsFork OR rootCollapse) */ export function computeRemoveExtensionGas(cellLoads: bigint, needsMergeGas = false): bigint { - const { baseGas } = getWalletGasParams(TonWalletVersion.V5R1); + const { baseGas } = walletGasParams(TonWalletVersion.V5R1); // baseGas(4222) + removeOverhead(1068) + cellLoads×(100 + 500) + mergeExtra // = 5290 + 600×cellLoads + (needsMergeGas ? 75 : 0) return ( @@ -774,63 +851,6 @@ export function computeRemoveLastExtensionGas(): bigint { return computeRemoveExtensionGas(1n) - TVM_CELL_RELOAD_GAS; } -/** - * Find sibling node after removing a key from trie. - * Returns { type: 'leaf' | 'fork', cellLoads } or null if key not found. - * - * The sibling is the other branch of the parent fork after deletion. - * When we delete a leaf, its parent fork merges with the sibling. - */ -function findDeleteInfo( - node: TrieNode | undefined, - key: string, - pos = 0 -): { cellLoads: number; siblingIsFork: boolean } | null { - if (!node) return null; - - // Count cell load for this node - const loads = 1; - - if (node.type === 'leaf') { - // Found the leaf to delete - but no sibling info here - // (sibling is determined by parent, which we track below) - return node.key === key ? { cellLoads: loads, siblingIsFork: false } : null; - } - - // Fork node - check if label matches - const nodeKeys = getAllKeys(node); - const labelBits = node.labelLength! - pos; - const nodePrefix = nodeKeys[0].slice(pos, node.labelLength); - const keySlice = key.slice(pos, node.labelLength); - - // Check common prefix length - let pfxLen = 0; - while (pfxLen < labelBits && nodePrefix[pfxLen] === keySlice[pfxLen]) pfxLen++; - - if (pfxLen < labelBits) { - // Mismatch - key not in trie - return null; - } - - // Label matches - continue to child - const nextBit = key[node.labelLength!]; - const child = nextBit === '0' ? node.left : node.right; - const sibling = nextBit === '0' ? node.right : node.left; - - const childResult = findDeleteInfo(child, key, node.labelLength! + 1); - if (!childResult) return null; - - // If child is the leaf being deleted, determine sibling type - if (child?.type === 'leaf' && child.key === key) { - // Sibling will be merged with parent fork - const siblingIsFork = sibling?.type === 'fork'; - return { cellLoads: loads + childResult.cellLoads, siblingIsFork }; - } - - // Propagate from deeper in the tree - return { cellLoads: loads + childResult.cellLoads, siblingIsFork: childResult.siblingIsFork }; -} - /** * Compute gas for V5R1 RemoveExtension action from existing extensions. * @@ -853,13 +873,6 @@ export function computeRemoveExtensionGasFromExtensions( return computeRemoveLastExtensionGas(); } - function hexToBinary(hex: string): string { - return hex - .split('') - .map(c => parseInt(c, 16).toString(2).padStart(4, '0')) - .join(''); - } - const binaryKeys = existingExtensionHashes.map(hexToBinary); const trie = buildTrie(binaryKeys); diff --git a/packages/core/src/service/ton-blockchain/fee/index.ts b/packages/core/src/service/ton-blockchain/fee/index.ts index c7f59371a..1e1a39dc1 100644 --- a/packages/core/src/service/ton-blockchain/fee/index.ts +++ b/packages/core/src/service/ton-blockchain/fee/index.ts @@ -1,24 +1,4 @@ -export { - UNINIT_ACCOUNT_STORAGE, - estimateWalletFee, - computeForwardFee, - computeImportFee, - computeActionFee, - computeGasFee, - computeStorageFee, - computeWalletGasUsed, - getWalletGasParams, - computeFwdFeeRemaining, - countUniqueCellStats, - sumRefsStats, - parseV5R1ExtensionAction, - computeAddExtensionGas, - computeAddFirstExtensionGas, - computeAddExtensionGasFromExtensions, - computeRemoveExtensionGas, - computeRemoveLastExtensionGas, - computeRemoveExtensionGasFromExtensions -} from './fees'; +export { UNINIT_ACCOUNT_STORAGE, estimateWalletFee } from './fees'; export type { MsgForwardPrices, From d9131e09e80fcff74b20ce2fdf15c2ef5574dde1 Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Fri, 6 Feb 2026 23:29:05 +0400 Subject: [PATCH 8/8] refactor(fees): improve fee fetching logic and enhance output formatting --- .../src/service/ton-blockchain/fee/__tests__/fees.spec.ts | 1 + .../fee/__tests__/fixtures/tonapi-fetcher.ts | 8 ++++++-- packages/core/src/service/ton-blockchain/fee/compat.ts | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts index c65d069f9..0b130bbbc 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fees.spec.ts @@ -311,6 +311,7 @@ describe('7. V5R1 Extension Gas', () => { describe('8. Blockchain-verified Transactions', () => { async function loadExpected(fixture: WalletFeeTestCase): Promise { if (shouldFetchRealFees()) { + // eslint-disable-next-line no-console console.log(`Fetching tx: ${fixture.txHash}`); return fetchExpectedFees(fixture.txHash); } diff --git a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts index 54951ed77..fd34335d8 100644 --- a/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts +++ b/packages/core/src/service/ton-blockchain/fee/__tests__/fixtures/tonapi-fetcher.ts @@ -46,11 +46,13 @@ export async function fetchExpectedFees(txHash: string): Promise { const response = await fetch(url); if (response.ok) { - return await response.json().then(parseFees); + const data = await response.json(); + return parseFees(data); } if (response.status === 429 && attempt < maxRetries) { const delay = baseDelay * Math.pow(2, attempt); + // eslint-disable-next-line no-console console.log(`Rate limited, retrying in ${delay}ms...`); await sleep(delay); continue; @@ -160,7 +162,9 @@ export function formatVerifyResult(result: VerifyResult): string { const format = (name: string, pred: bigint, act: bigint, diff: bigint): string => { const diffStr = diff === 0n ? '0' : diff > 0n ? `+${diff}` : `${diff}`; - return `${name.padEnd(15)} | ${String(pred).padStart(9)} | ${String(act).padStart(9)} | ${diffStr}`; + return `${name.padEnd(15)} | ${String(pred).padStart(9)} | ${String(act).padStart( + 9 + )} | ${diffStr}`; }; // Reconstruct predicted from actual - diff diff --git a/packages/core/src/service/ton-blockchain/fee/compat.ts b/packages/core/src/service/ton-blockchain/fee/compat.ts index 00fd5a4bf..8943f457b 100644 --- a/packages/core/src/service/ton-blockchain/fee/compat.ts +++ b/packages/core/src/service/ton-blockchain/fee/compat.ts @@ -9,7 +9,6 @@ export { assertUnreachable } from '../../../utils/types'; /** - * TonWalletVersion mirrors the multiplatform enum exactly. * The web repo uses WalletVersion (numeric enum) from entries/wallet.ts, * but this module uses the string enum for algorithm compatibility. */