diff --git a/docs.json b/docs.json index 2ec75eb9..100a726a 100644 --- a/docs.json +++ b/docs.json @@ -486,7 +486,8 @@ }, "foundations/hypercube-routing", "foundations/statuses", - "foundations/phases-and-fees", + "foundations/phases", + "foundations/fees", "foundations/shards", "foundations/limits", "foundations/config", diff --git a/ecosystem/blueprint/benchmarks.mdx b/ecosystem/blueprint/benchmarks.mdx index 387627f0..c1592c3c 100644 --- a/ecosystem/blueprint/benchmarks.mdx +++ b/ecosystem/blueprint/benchmarks.mdx @@ -13,11 +13,7 @@ Unlike many other blockchains, TON also requires you to pay for storing contract As you develop and iterate on a contract, even small changes to its logic can affect both gas usage and data size. Monitoring these changes helps ensure that your contract remains efficient and cost-effective. ## Gas metrics reporting @@ -162,7 +158,7 @@ storage_fee = ceil( * time_delta / 2 ** 16) ``` -To try this in practice, use the [calculator example](/foundations/phases-and-fees). +To try this in practice, use the [calculator example](/foundations/fees). ### Regenerate the gas report diff --git a/foundations/fees.mdx b/foundations/fees.mdx new file mode 100644 index 00000000..64ac33fa --- /dev/null +++ b/foundations/fees.mdx @@ -0,0 +1,377 @@ +--- +title: "Transaction fees" +--- + +import { Aside } from '/snippets/aside.jsx'; + +## Transaction fees + +Fees on TON are calculated using this formula: + +```cpp title="FORMULAS" +transaction_fee = storage_fees + + in_fwd_fees // also called import fee + + computation_fees + + action_fees + + out_fwd_fees +``` + +All fees are denominated in nanotons (often scaled by `2^16` for precision) and come from network configuration: + +- `storage_fees`: param [18](https://tonviewer.com/config#18) + +- `in_fwd_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) + +- `computation_fees`: params [20](https://tonviewer.com/config#20) and [21](https://tonviewer.com/config#21) + +- `action_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) + +- `out_fwd_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) + +- `storage_fees` is the amount you pay for storing a smart contract on the blockchain. In fact, you pay for every second the smart contract is stored on the blockchain. + - _Example_: your TON wallet is also a smart contract, and it pays a storage fee every time you receive or send a transaction. + +- `in_fwd_fees` is a charge for importing messages only from outside the blockchain, for example, `external` messages. Every time you make a transaction, it must be delivered to the validators who will process it. For ordinary messages from contract to contract, this fee does not apply. Read [the TON Blockchain paper](/resources/pdfs/tblkch.pdf) to learn more about inbound messages. + - _Example_: each transaction you make with your wallet app (like Tonkeeper) must first be distributed among validators. + +- `computation_fees` is the amount you pay for executing code in the virtual machine. Computation fees depend on executed operations (gas used), not code size. + - _Example_: each time you send a transaction with your wallet (which is a smart contract), you execute the code of your wallet contract and pay for it. + +- `action_fees` is a charge for sending outgoing messages made by a smart contract, updating the smart contract code, updating libraries, etc. + +- `out_fwd_fees` is a charge for forwarding outgoing internal messages within TON between shardchains; it depends on message size. + +## Storage fee + +Storage fee for smart contracts is calculated using the following formula, values are defined in network config param [`18`](https://tonviewer.com/config#18): + +```cpp title="FORMULAS" +storage_fee = ceil( + (account.bits * bit_price + + account.cells * cell_price) + * time_delta / 2^16) +``` + +```ts title="TypeScript example" +import { Cell, beginCell } from '@ton/core'; + +// Read latest storage prices (config param 18) +const storage = getStoragePrices(configCell); + +// Account state as a Cell (e.g., code+data root) +const accountRoot: Cell = /* load from blockchain */; +const stats = collectCellStats(accountRoot, []); + +// Charge for one hour +const fee = shr16ceil( + (stats.bits * storage.bit_price_ps + stats.cells * storage.cell_price_ps) * + 3600n +); +``` + +> See [Helper functions appendix](/foundations/fees#helper-functions) for full implementations. + + + +## Gas fee + +All computation is measured in gas units; each TVM operation has a fixed gas cost.\ +The gas price is defined by network configuration and **can not be set by users**. + +- Basechain: 1 gas = `26214400 / 2^16` nanotons = 0.0000004 TON +- Masterchain: 1 gas = `655360000 / 2^16` nanotons = 0.00001 TON + +See config parameters [`20`](https://tonviewer.com/config#20) and [`21`](https://tonviewer.com/config#21) for current gas prices.\ +The values can change through validator governance. + +```ts title="TypeScript example" +const gasUsed = 50_000n; +const prices = getGasPrices(configCell, 0); // 0 = basechain +const gasFee = + gasUsed <= prices.flat_gas_limit + ? prices.flat_gas_price + : prices.flat_gas_price + + (prices.gas_price * (gasUsed - prices.flat_gas_limit)) / 65536n; +``` + +> See [Helper functions appendix](/foundations/fees#helper-functions) for full implementations. + +## Forward fee + +Forward fee for message size (`msg.bits`, `msg.cells`) per params [`24`](https://tonviewer.com/config#24)/[`25`](https://tonviewer.com/config#25): + +```cpp title="FORMULAS" +// bits in the root cell of a message are not included in msg.bits (lump_price pays for them) +msg_fwd_fees = (lump_price + + ceil( + (bit_price * msg.bits + cell_price * msg.cells) / 2^16) + ); + +``` + +```ts title="TypeScript example" +const msgPrices = getMsgPrices(configCell, 0); +const total = + msgPrices.lumpPrice + + shr16ceil( + msgPrices.bitPrice * BigInt(bits) + + msgPrices.cellPrice * BigInt(cells) + ); +const actionFee = (total * msgPrices.firstFrac) >> 16n; +const forwardFee = total - actionFee; + +// From a Cell or Message: +const fwdFromCell = computeCellForwardFees(msgPrices, messageRootCell); +// For a full internal Message object (validates default lump case, handles init): +const details = computeMessageForwardFees(msgPrices, internalMessage); +``` + + + +### Import fee + +Import fee is the same as forward fee for inbound external messages. + + + +## Action fee + +Action fee is charged in the Action phase and is the sender’s share of the forward fee.\ +You pay it for `SENDRAWMSG`; actions like `RAWRESERVE`, `SETCODE` do not incur the fees. + +```cpp title="FORMULAS" +action_fee = floor((msg_fwd_fees * first_frac)/ 2^16); // internal + +action_fee = msg_fwd_fees; // external +``` + +`first_frac` (params [`24`](https://tonviewer.com/config#24)/[`25`](https://tonviewer.com/config#25)) divided by `2^16` ≈ 1/3 of `msg_fwd_fees`. + +Action fine (failed send): Starting with Global Version 4, a failed "send message" action incurs a penalty proportional to the attempted message size. It is calculated as: + +```cpp title="FORMULAS" +fine_per_cell = floor((cell_price >> 16) / 4) +max_cells = floor(remaining_balance / fine_per_cell) +action_fine = fine_per_cell * min(max_cells, cells_in_msg); +``` + +## Helper functions (full code) + +```ts expandable +import { Cell, Slice, beginCell, Dictionary, Message, DictionaryValue } from '@ton/core'; + +export type GasPrices = { + flat_gas_limit: bigint, + flat_gas_price: bigint, + gas_price: bigint +}; + +export type StorageValue = { + utime_since: number, + bit_price_ps: bigint, + cell_price_ps: bigint, + mc_bit_price_ps: bigint, + mc_cell_price_ps: bigint +}; + +export class StorageStats { + bits: bigint; + cells: bigint; + + constructor(bits?: number | bigint, cells?: number | bigint) { + this.bits = bits !== undefined ? BigInt(bits) : 0n; + this.cells = cells !== undefined ? BigInt(cells) : 0n; + } + add(...stats: StorageStats[]) { + let cells = this.cells, bits = this.bits; + for (let stat of stats) { + bits += stat.bits; + cells += stat.cells; + } + return new StorageStats(bits, cells); + } + addBits(bits: number | bigint) { + return new StorageStats(this.bits + BigInt(bits), this.cells); + } + addCells(cells: number | bigint) { + return new StorageStats(this.bits, this.cells + BigInt(cells)); + } +} + +function shr16ceil(src: bigint) { + const rem = src % 65536n; + let res = src / 65536n; + if (rem !== 0n) res += 1n; + return res; +} + +export function collectCellStats(cell: Cell, visited: Array, skipRoot: boolean = false): StorageStats { + let bits = skipRoot ? 0n : BigInt(cell.bits.length); + let cells = skipRoot ? 0n : 1n; + const hash = cell.hash().toString(); + if (visited.includes(hash)) { + return new StorageStats(); + } + visited.push(hash); + for (const ref of cell.refs) { + const r = collectCellStats(ref, visited); + cells += r.cells; + bits += r.bits; + } + return new StorageStats(bits, cells); +} + +export function getGasPrices(configRaw: Cell, workchain: 0 | -1): GasPrices { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const ds = config.get(21 + workchain)!.beginParse(); + if (ds.loadUint(8) !== 0xd1) throw new Error('Invalid flat gas prices tag'); + const flat_gas_limit = ds.loadUintBig(64); + const flat_gas_price = ds.loadUintBig(64); + if (ds.loadUint(8) !== 0xde) throw new Error('Invalid gas prices tag'); + return { flat_gas_limit, flat_gas_price, gas_price: ds.preloadUintBig(64) }; +} + +export function computeGasFee(prices: GasPrices, gas: bigint): bigint { + if (gas <= prices.flat_gas_limit) return prices.flat_gas_price; + return prices.flat_gas_price + (prices.gas_price * (gas - prices.flat_gas_limit)) / 65536n; +} + +export const storageValue: DictionaryValue = { + serialize: (src, builder) => { + builder + .storeUint(0xcc, 8) + .storeUint(src.utime_since, 32) + .storeUint(src.bit_price_ps, 64) + .storeUint(src.cell_price_ps, 64) + .storeUint(src.mc_bit_price_ps, 64) + .storeUint(src.mc_cell_price_ps, 64); + }, + parse: (src) => { + return { + utime_since: src.skip(8).loadUint(32), + bit_price_ps: src.loadUintBig(64), + cell_price_ps: src.loadUintBig(64), + mc_bit_price_ps: src.loadUintBig(64), + mc_cell_price_ps: src.loadUintBig(64) + }; + } +}; + +export function getStoragePrices(configRaw: Cell): StorageValue { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32), storageValue, config.get(18)!); + const values = storageData.values(); + return values[values.length - 1]; +} + +export function calcStorageFee(prices: StorageValue, stats: StorageStats, duration: bigint) { + return shr16ceil((stats.bits * prices.bit_price_ps + stats.cells * prices.cell_price_ps) * duration); +} + +export const configParseMsgPrices = (sc: Slice) => { + const magic = sc.loadUint(8); + if (magic !== 0xea) throw new Error('Invalid message prices magic number'); + return { + lumpPrice: sc.loadUintBig(64), + bitPrice: sc.loadUintBig(64), + cellPrice: sc.loadUintBig(64), + ihrPriceFactor: sc.loadUintBig(32), + firstFrac: sc.loadUintBig(16), + nextFrac: sc.loadUintBig(16) + }; +}; + +export type MsgPrices = ReturnType; + +export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1) => { + const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); + const prices = config.get(25 + workchain); + if (prices === undefined) throw new Error('No prices defined in config'); + return configParseMsgPrices(prices.beginParse()); +}; + +export function computeDefaultForwardFee(msgPrices: MsgPrices) { + return msgPrices.lumpPrice - ((msgPrices.lumpPrice * msgPrices.firstFrac) >> 16n); +} + +export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) { + return msgPrices.lumpPrice + shr16ceil(msgPrices.bitPrice * bits + msgPrices.cellPrice * cells); +} + +export function computeFwdFeesVerbose(msgPrices: MsgPrices, cells: bigint | number, bits: bigint | number) { + const fees = computeFwdFees(msgPrices, BigInt(cells), BigInt(bits)); + const res = (fees * msgPrices.firstFrac) >> 16n; + return { total: fees, res, remaining: fees - res }; +} + +export function computeCellForwardFees(msgPrices: MsgPrices, msg: Cell) { + const storageStats = collectCellStats(msg, [], true); + return computeFwdFees(msgPrices, storageStats.cells, storageStats.bits); +} + +export function computeMessageForwardFees(msgPrices: MsgPrices, msg: Message) { + if (msg.info.type !== 'internal') throw new Error('Helper intended for internal messages'); + let storageStats = new StorageStats(); + + const defaultFwd = computeDefaultForwardFee(msgPrices); + if (msg.info.forwardFee === defaultFwd) { + return { + fees: msgPrices.lumpPrice, + res: defaultFwd, + remaining: defaultFwd, + stats: storageStats + }; + } + + const visited: Array = []; + + if (msg.init) { + let addBits = 5n; + let refCount = 0; + if (msg.init.splitDepth) addBits += 5n; + if (msg.init.libraries) { + refCount++; + storageStats = storageStats.add( + collectCellStats(beginCell().storeDictDirect(msg.init.libraries).endCell(), visited, true) + ); + } + if (msg.init.code) { + refCount++; + storageStats = storageStats.add(collectCellStats(msg.init.code, visited)); + } + if (msg.init.data) { + refCount++; + storageStats = storageStats.add(collectCellStats(msg.init.data, visited)); + } + if (refCount >= 2) { + storageStats = storageStats.addCells(1).addBits(addBits); + } + } + + const lumpBits = BigInt(msg.body.bits.length); + const bodyStats = collectCellStats(msg.body, visited, true); + storageStats = storageStats.add(bodyStats); + + let feesVerbose = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits); + if (feesVerbose.remaining < msg.info.forwardFee) { + storageStats = storageStats.addCells(1).addBits(lumpBits); + feesVerbose = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits); + } + if (feesVerbose.remaining !== msg.info.forwardFee) { + throw new Error('Forward fee calculation mismatch'); + } + return { fees: feesVerbose, stats: storageStats }; +} +``` diff --git a/foundations/phases.mdx b/foundations/phases.mdx new file mode 100644 index 00000000..15dcb3f4 --- /dev/null +++ b/foundations/phases.mdx @@ -0,0 +1,201 @@ +--- +title: "Execution phases" +--- + +import { Aside } from '/snippets/aside.jsx'; + +When an event occurs on an account in The Open Network (TON) blockchain, it triggers a **transaction**. +The most common event is receiving a message, but other events like `tick-tock`, `merge`, and `split` can also initiate transactions. + +Each transaction consists of up to five phases: + +1. **Storage phase**: calculates storage fees for the contract based on the space it occupies in the blockchain state. +1. **Credit phase**: updates the contract balance by accounting for incoming message values and storage fees. +1. **Compute phase**: executes the contract code on the TON Virtual Machine (TVM). The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. +1. **Action phase**: processes actions from the compute phase if it succeeds.\ + Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. +1. **Bounce phase**: If the compute phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. If the send\_msg action failed and it had the +16 flag set, then the bounce phase will also be triggered. + +> Compute, Action and Bounce phases may be skipped + + + +## Fee deduction sequence + +1. Import fee (before the first phase) +1. Storage fee (storage phase) +1. Gas fee (compute phase) +1. Action fee + forward fee (action phase) +1. Additional forward fees (bounce phase) + +## Storage phase + +Cannot be skipped. + +In this phase, the blockchain processes fees related to the account's persistent storage. Let's start by looking at the TL-B schema: + +```tlb +tr_phase_storage$_ storage_fees_collected:Grams + storage_fees_due:(Maybe Grams) + status_change:AccStatusChange + = TrStoragePhase; +``` + +The `storage_fees_due` field is of type `Maybe` because it is only present when the account has **insufficient balance** to cover the storage fees. When the account has enough funds, this field is omitted. + +> Note: Grams are unsigned integers, so account balances cannot be negative. + +The `AccStatusChange` field indicates whether the account's status changed during this phase. For example, see [account status variety](/foundations/statuses#status-variety). + +## Credit phase + +Cannot be skipped. + +This phase is relatively small and straightforward. +The main logic of this phase is to **credit the contract’s balance** with the remaining value from the incoming message. + +The credit phase is serialized in TL-B as follows: + +```tlb +tr_phase_credit$_ due_fees_collected:(Maybe Grams) + credit:CurrencyCollection = TrCreditPhase; +``` + +This phase consists of the following two fields: + +| Field | Type | Description | +| -------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `due_fees_collected` | `Maybe Grams` | Amount of previously due storage fees collected (present only if storage fees were due and collected in this phase). | +| `credit` | `CurrencyCollection` | The amount credited to the account as a result of receiving the incoming message. | + +## Compute phase + +The **compute phase** is one of the most complex stages of a transaction. This is where the smart contract code, stored in the account’s state, is executed. + +Unlike previous phases, the TL-B definition for the compute phase includes multiple variants. + +```tlb +tr_phase_compute_skipped$0 reason:ComputeSkipReason + = TrComputePhase; +tr_phase_compute_vm$1 success:Bool msg_state_used:Bool + account_activated:Bool gas_fees:Grams + ^[ gas_used:(VarUInteger 7) + gas_limit:(VarUInteger 7) gas_credit:(Maybe (VarUInteger 3)) + mode:int8 exit_code:int32 exit_arg:(Maybe int32) + vm_steps:uint32 + vm_init_state_hash:bits256 vm_final_state_hash:bits256 ] + = TrComputePhase; +cskip_no_state$00 = ComputeSkipReason; +cskip_bad_state$01 = ComputeSkipReason; +cskip_no_gas$10 = ComputeSkipReason; +cskip_suspended$110 = ComputeSkipReason; +``` + +### When the compute phase is skipped + +To start, note that the compute phase can be **skipped** entirely. In that case, the reason for skipping is explicitly recorded and can be one of the following: + +| Skip reason | Description | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cskip_no_state` | The smart contract has no [state](/foundations/statuses#status-variety) and, therefore, no code, so execution is not possible. | +| `cskip_bad_state` | Raised in two cases: when the `fixed_prefix_length` [field has an invalid value][fixed_prefix_length] or when the `StateInit` provided in the incoming message [does not match the account’s address][account_address]. | +| `cskip_no_gas` | The incoming message did not provide enough TON to cover the gas required to execute the smart contract. | +| `cskip_suspended` | The address is suspended; execution is disabled (used to limit early miner accounts). | + +[fixed_prefix_length]: https://github.com/ton-blockchain/ton/blob/72056a2261cbb11f7cf0f20b389bcbffe018b1a8/crypto/block/transaction.cpp#L1721 + +[account_address]: https://github.com/ton-blockchain/ton/blob/72056a2261cbb11f7cf0f20b389bcbffe018b1a8/crypto/block/transaction.cpp#L1726 + + + +## Action phase + +Once the smart contract code has finished executing, the **Action phase** begins. If any actions were created during the compute phase, they are processed at this stage. + +There are precisely 4 types of actions in TON: + +```tlb +action_send_msg#0ec3c86d mode:(## 8) + out_msg:^(MessageRelaxed Any) = OutAction; +action_set_code#ad4de08e new_code:^Cell = OutAction; +action_reserve_currency#36e6b809 mode:(## 8) + currency:CurrencyCollection = OutAction; +libref_hash$0 lib_hash:bits256 = LibRef; +libref_ref$1 library:^Cell = LibRef; +action_change_library#26fa1dd4 mode:(## 7) + libref:LibRef = OutAction; +``` + +| Type | Description | +| ------------------------- | ------------------------------------------------------------------------------------------ | +| `action_send_msg` | Sends a message. | +| `action_set_code` | Updates the smart contract's code. | +| `action_reserve_currency` | Reserves a portion of the account's balance. This is especially useful for gas management. | +| `action_change_library` | Changes the library used by the smart contract. | + +These actions are executed _in the order in which they were created_ during code execution. +A total of up to [255 actions](https://github.com/ton-blockchain/ton/blob/cac968f77dfa5a14e63db40190bda549f0eaf746/crypto/block/transaction.h#L164) can be made. + +Here is the TL-B schema, which defines the structure of the action phase: + +```tlb +tr_phase_action$_ success:Bool valid:Bool no_funds:Bool + status_change:AccStatusChange + total_fwd_fees:(Maybe Grams) total_action_fees:(Maybe Grams) + result_code:int32 result_arg:(Maybe int32) tot_actions:uint16 + spec_actions:uint16 skipped_actions:uint16 msgs_created:uint16 + action_list_hash:bits256 tot_msg_size:StorageUsed + = TrActionPhase; +``` + +## Bounce phase + +If the **Compute phase** or **Action phase** ends with an error, and the incoming message has the `bounce` flag set, the system triggers the **Bounce phase**. + + + +```tlb +tr_phase_bounce_negfunds$00 = TrBouncePhase; +tr_phase_bounce_nofunds$01 msg_size:StorageUsed + req_fwd_fees:Grams = TrBouncePhase; +tr_phase_bounce_ok$1 msg_size:StorageUsed + msg_fees:Grams fwd_fees:Grams = TrBouncePhase; +``` + +The `tr_phase_bounce_negfunds` type is not used in the current version of the blockchain. The other two types function as follows: + +| Type | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `tr_phase_bounce_nofunds` | Indicates that the account does not have enough funds to process the message that should be bounced back to the sender. | +| `tr_phase_bounce_ok` | Indicates that the system successfully processes the bounce and sends the message back to the sender. | + +In this phase, `msg_fees` and `fwd_fees` are calculated from the `total_fwd_fees`:\ +approximately $\frac{1}{3}$ goes to `msg_fees` and $\frac{2}{3}$ go to `fwd_fees`. + +> See [Fees → Forward fee](/foundations/fees#forward-fee) for more info. + +### Key points + +- If the receiver cannot parse the message and terminates with a non-zero exit code, the message bounces back automatically. +- The bounced message has its `bounce` flag cleared and `bounced` flag set, and contains `0xffffffff` (32-bit) op code followed by the original message body. +- Always check the `bounced` flag before parsing `op` to avoid treating a bounce as a new query. diff --git a/patterns/fee-management.mdx b/patterns/fee-management.mdx new file mode 100644 index 00000000..e69de29b diff --git a/resources/dictionaries/custom.txt b/resources/dictionaries/custom.txt index 844488b6..14902d00 100644 --- a/resources/dictionaries/custom.txt +++ b/resources/dictionaries/custom.txt @@ -595,6 +595,7 @@ relatedly relayer resharding resynced +retracer rollup ruleset runbooks @@ -681,6 +682,7 @@ trie truthy ts tsUSDe +txtracer uint unary unblockable diff --git a/snippets/feePlayground.jsx b/snippets/feePlayground.jsx new file mode 100644 index 00000000..8f59ca57 --- /dev/null +++ b/snippets/feePlayground.jsx @@ -0,0 +1,166 @@ +export const FeePlayground = () => { + const nano = 10 ** -9; + const bit16 = 2 ** 16; + const Note = ({ title, children }) => ( +
+ {title && ( +
+ {title} +
+ )} +
+ {children} +
+
+ ); + + const compute = (form) => { + const presets = { + // #25 - Messages prices (basechain) + basechain: { + lump_price: 400000, + bit_price: 26214400, + cell_price: 2621440000, + first_frac: 21845, + next_frac: 21845, + }, + // #24 - Masterchain messages prices + masterchain: { + lump_price: 10000000, + bit_price: 655360000, + cell_price: 65536000000, + first_frac: 21845, + next_frac: 21845, + }, + }; + const storagePrices = { + basechain: { bit_ps: 1, cell_ps: 500 }, + masterchain: { bit_ps: 1000, cell_ps: 500000 }, + }; + + const net = form.network.value === 'masterchain' ? 'masterchain' : 'basechain'; + const { lump_price: lumpPrice, bit_price: bitPrice, cell_price: cellPrice, first_frac: firstFrac } = presets[net]; + + const importBits = Number(form.import_bits.value || 0); + const importCells = Number(form.import_cells.value || 0); + const fwdBits = Number(form.fwd_bits.value || 0); + const fwdCells = Number(form.fwd_cells.value || 0); + + const gasFeesTon = Number(form.gas_fees_ton.value || 0); + + // Account storage parameters + const accountBits = Number(form.account_bits.value || 0); + const accountCells = Number(form.account_cells.value || 0); + const { bit_ps, cell_ps } = storagePrices[net]; + + const timeDelta = Number(form.time_delta.value || 69); + + // Compute storage fee from account params (nanotons) + const storageFeeNano = Math.ceil(((accountBits * bit_ps + accountCells * cell_ps) * timeDelta) / bit16); + const storageFeesTon = storageFeeNano * nano; + // storage fee is displayed in the results area only + + const fwdFee = lumpPrice + Math.ceil((bitPrice * fwdBits + cellPrice * fwdCells) / bit16); + const totalFwdFees = fwdFee; + const totalActionFees = +((fwdFee * firstFrac) / bit16).toFixed(9); + const importFee = lumpPrice + Math.ceil((bitPrice * importBits + cellPrice * importCells) / bit16); + const totalFeeTon = gasFeesTon + storageFeesTon + importFee * nano + totalFwdFees * nano; + + const setOut = (key, value) => { + const el = form.querySelector(`[data-out="${key}"]`); + if (el) el.textContent = value; + }; + + setOut('total', totalFeeTon.toFixed(9)); + setOut('action', (totalActionFees * nano).toFixed(9)); + setOut('fwd', (totalFwdFees * nano).toFixed(9)); + setOut('import', (importFee * nano).toFixed(9)); + setOut('gas', gasFeesTon.toFixed(9)); + setOut('storage', storageFeesTon.toFixed(9)); + }; + + const init = (node) => { + if (node) compute(node); + }; + + return ( +
compute(e.currentTarget)} className="not-prose my-4 p-4 border rounded-xl dark:border-white/20 border-black/10"> +
+
Network
+
+ +
+
+ +
+
+

Import Payload

+ + +
+ +
+

Forward Payload

+ + +
+
+ +
+

Account storage

+
+ + + +
+ + You can find import, forward and storage parameters in the Executor logs (txtracer/retracer) for a specific transaction. + +
+ +
+

Compute fee

+ + The compute (gas) cost cannot be predicted by a static formula.
+ Measure it in tests or read it from the executor logs / explorer, then enter the gas fee here. +
+
+ +
+
+ +
+
+
Fwd. fee: TON
+
Gas fee: TON
+
Storage fee: TON
+
Action fee: TON
+
+
+
Import fee: TON
+
Total fee: TON
+
+
+
+ ); +}; +