UNPKG

@ledgerhq/coin-tezos

Version:
279 lines 14.3 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import { log } from '@ledgerhq/logs'; import { COST_PER_BYTE, getRevealFee, ORIGINATION_SIZE } from '@taquito/taquito'; import { validatePublicKey, ValidationResult } from '@taquito/utils'; import { UnsupportedTransactionMode } from '../types/errors'; import { computeMaxStakeAmount, createFallbackEstimation, createMockSigner, DUST_MARGIN_MUTEZ, MIN_SUGGESTED_FEE_SMALL_TRANSFER, OP_SIZE_XTZ_TRANSFER, normalizePublicKeyForAddress, partitionNativeBalance, } from '../utils'; import { getTezosToolkit } from './tezosToolkit'; /** * Fetch the transaction fees for a transaction * * @param {Account} account * @param {Transaction} transaction */ export async function estimateFees(context, { account, transaction, }) { const config = await context.config(); // Normalize public key (hex -> base58) when provided (may be undefined for unrevealed accounts) // before the device is connected const encodedPubKey = account.xpub ? normalizePublicKeyForAddress(account.xpub, account.address) : undefined; const tezosToolkit = getTezosToolkit(config); if (encodedPubKey && validatePublicKey(encodedPubKey) === ValidationResult.VALID) { tezosToolkit.setProvider({ signer: createMockSigner(account.address, encodedPubKey) }); } else { tezosToolkit.setProvider({ signer: createMockSigner(account.address, '') }); } const estimation = { fees: 0n, gasLimit: 0n, storageLimit: 0n, estimatedFees: 0n, }; // For legacy compatibility if (account.balance === 0n) { return transaction.useAllAmount ? { ...estimation, amount: 0n } : estimation; } const spendableForMax = Number(partitionNativeBalance(account.balance, account.stakedBalance ?? 0n, account.unstakedBalance ?? 0n).spendable); let amount = transaction.amount; const coerceMinAmountForEstimation = (transaction.useAllAmount && (transaction.mode === 'send' || transaction.mode === 'stake' || transaction.mode === 'unstake')) || (amount === 0n && transaction.mode !== 'send_token'); if (coerceMinAmountForEstimation) { amount = 1n; // send/stake max or zero-amount pre-estimation (taquito refuses 0); not used for FA2 send_token } try { if ((transaction.mode === 'send' || transaction.mode === 'send_token') && !transaction.recipient) { return { ...estimation, ...createFallbackEstimation(config), }; } let estimate; switch (transaction.mode) { case 'send': estimate = await tezosToolkit.estimate.transfer({ mutez: true, to: transaction.recipient, amount: Number(amount), source: account.address, // avoid requiring signer for estimation storageLimit: ORIGINATION_SIZE, // https://github.com/TezTech/eztz/blob/master/PROTO_003_FEES.md for originating an account }); break; case 'delegate': estimate = await tezosToolkit.estimate.setDelegate({ source: account.address, delegate: transaction.recipient, }); break; case 'undelegate': estimate = await tezosToolkit.estimate.setDelegate({ source: account.address, }); break; case 'stake': estimate = await tezosToolkit.estimate.stake({ amount: Number(amount), mutez: true, }); break; case 'unstake': estimate = await tezosToolkit.estimate.unstake({ amount: Number(amount), mutez: true, }); break; case 'finalize_unstake': estimate = await tezosToolkit.estimate.finalizeUnstake({}); break; case 'send_token': { if (!transaction.contractAddress || transaction.tokenId === undefined) { throw new Error('FA2 transfer requires contractAddress and tokenId'); } const tokenContract = await tezosToolkit.contract.at(transaction.contractAddress); const transferParams = tokenContract.methods .transfer([ { from_: account.address, txs: [ { to_: transaction.recipient, token_id: transaction.tokenId, amount: amount, }, ], }, ]) .toTransferParams({ mutez: true }); estimate = await tezosToolkit.estimate.transfer({ ...transferParams, source: account.address, }); break; } default: throw new UnsupportedTransactionMode('unsupported mode', { mode: transaction.mode }); } const minFees = config.fees.minFees ?? 0; const mainOpFee = Math.max(minFees, estimate.suggestedFeeMutez); const revealFee = account.revealed ? 0n : BigInt(getRevealFeeForEstimation(config, account.address)); // NOTE: send-max only applies to native XTZ transfer, not FA2 if (transaction.useAllAmount && transaction.mode === 'send') { // Reserve `mainOpFee`, not the raw taquito suggestion: `estimatedFees` below reports the // `minFees` floor, so subtracting the (lower) suggestion here yields an amount whose // `amount + estimatedFees` exceeds the spendable balance, and validateIntent then rejects // the very max it was handed (LIVE-28506). // NOTE: from https://github.com/ecadlabs/taquito/blob/master/integration-tests/__tests__/contract/empty-implicit-account-into-new-implicit-account.spec.ts#L37 const totalFees = estimate.burnFeeMutez > 0 ? mainOpFee + estimate.burnFeeMutez - 20 * COST_PER_BYTE // 20 is storage buffer : mainOpFee; const maxAmount = spendableForMax - (totalFees + Number(revealFee)); // NOTE: from https://github.com/ecadlabs/taquito/blob/a70c64c4b105381bb9f1d04c9c70e8ef26e9241c/integration-tests/contract-empty-implicit-account-into-new-implicit-account.spec.ts#L33 // Temporary fix, see https://gitlab.com/tezos/tezos/-/issues/1754 // we need to increase the gasLimit and fee returned by the estimation const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1; const incr = DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize); const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr); estimation.amount = maxMinusBuff > 0 ? BigInt(maxMinusBuff) : 0n; log('tezos-send-max', 'send-max fee inputs', { minFees, mainOpFee, suggestedFeeMutez: estimate.suggestedFeeMutez, burnFeeMutez: estimate.burnFeeMutez, opSize: Number(estimate.opSize), revealFee: Number(revealFee), }); } else if (transaction.useAllAmount && transaction.mode === 'stake') { estimation.amount = computeMaxStakeAmount(BigInt(account.balance), account.stakedBalance ?? 0n, account.unstakedBalance ?? 0n, BigInt(mainOpFee) + revealFee); } else if (transaction.useAllAmount && transaction.mode === 'unstake') { // unstake-max draws from the staked balance, not the spendable balance estimation.amount = account.stakedBalance ?? 0n; } else { estimation.amount = transaction.amount; } estimation.fees = BigInt(mainOpFee); estimation.gasLimit = BigInt(estimate.gasLimit); estimation.storageLimit = BigInt(estimate.storageLimit); estimation.estimatedFees = estimation.fees + revealFee; } catch (e) { if (typeof e !== 'object' || !e) throw e; if ('id' in e) { estimation.taquitoError = e.id; log('taquito-error', 'taquito got error ' + e.id); } else if ('status' in e) { const errorMessage = String(e.message || ''); if (errorMessage.includes('Public key not found') || errorMessage.includes('wallet or contract API')) { log('taquito-network-error', 'Recipient address not found (new account), using default fees', { transaction: transaction, }); const fallback = createFallbackEstimation(config); estimation.fees = fallback.fees; estimation.gasLimit = fallback.gasLimit; estimation.storageLimit = fallback.storageLimit; estimation.estimatedFees = fallback.fees; if (!account.revealed) { estimation.estimatedFees = estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address)); } // Handle useAllAmount also for send mode when estimation falls back if (transaction.useAllAmount && transaction.mode === 'send') { // Approximate Taquito behavior for send-max using stable constants const suggestedFee = transaction.mode === 'send' ? MIN_SUGGESTED_FEE_SMALL_TRANSFER : Number(estimation.fees); // For display consistency in tests, align fees to suggestedFee in send-max if (transaction.mode === 'send') { estimation.fees = BigInt(suggestedFee); estimation.estimatedFees = BigInt(suggestedFee); if (!account.revealed) { estimation.estimatedFees = estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address)); } } const burnFeeMutez = Number(estimation.storageLimit) * COST_PER_BYTE; const totalFees = suggestedFee + (burnFeeMutez > 0 ? burnFeeMutez - 20 * COST_PER_BYTE : 0); const revealFee = account.revealed ? 0 : getRevealFeeForEstimation(config, account.address); const maxAmount = spendableForMax - (totalFees + revealFee); const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1; const incr = OP_SIZE_XTZ_TRANSFER + DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ; const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr); estimation.amount = maxMinusBuff > 0 ? BigInt(Math.floor(maxMinusBuff)) : 0n; } else { // preserve input amount in fallback for readability/tests estimation.amount = transaction.amount; } } else { log('taquito-network-error', errorMessage, { transaction: transaction, }); throw e; } } else { const msg = String(e.message || ''); if (msg.includes('No signer has been configured')) { const fallback = createFallbackEstimation(config); estimation.fees = fallback.fees; estimation.gasLimit = fallback.gasLimit; estimation.storageLimit = fallback.storageLimit; estimation.estimatedFees = fallback.estimatedFees; if (!account.revealed) { estimation.estimatedFees = estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address)); } if (transaction.useAllAmount && transaction.mode === 'send') { const suggestedFee = transaction.mode === 'send' ? MIN_SUGGESTED_FEE_SMALL_TRANSFER : Number(estimation.fees); if (transaction.mode === 'send') { estimation.fees = BigInt(suggestedFee); estimation.estimatedFees = BigInt(suggestedFee); if (!account.revealed) { estimation.estimatedFees = estimation.estimatedFees + BigInt(getRevealFeeForEstimation(config, account.address)); } } const burnFeeMutez = Number(estimation.storageLimit) * COST_PER_BYTE; const totalFees = suggestedFee + (burnFeeMutez > 0 ? burnFeeMutez - 20 * COST_PER_BYTE : 0); const revealFee = account.revealed ? 0 : getRevealFeeForEstimation(config, account.address); const maxAmount = spendableForMax - (totalFees + revealFee); const MINIMAL_FEE_PER_GAS_MUTEZ = 0.1; const incr = OP_SIZE_XTZ_TRANSFER + DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ; const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr); estimation.amount = maxMinusBuff > 0 ? BigInt(Math.floor(maxMinusBuff)) : 0n; } else { // preserve input amount in fallback for readability/tests estimation.amount = transaction.amount; } } else { throw e; } } } return estimation; } function getRevealFeeForEstimation(config, address) { const minFees = config.fees.minFees ?? 0; return Math.max(minFees, getRevealFee(address)); } //# sourceMappingURL=estimateFees.js.map