@ledgerhq/coin-tezos
Version:
893 lines (793 loc) • 27.2 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import type { TezosCoinConfig, TezosContext } from '../config'
import { getRevealFee } from '@taquito/taquito'
import { normalizePublicKeyForAddress, STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
import { estimateFees } from './estimateFees'
import { getTezosToolkit } from './tezosToolkit'
jest.mock('./tezosToolkit')
const defaultFeesConfig = {
minGasLimit: 600,
minRevealGasLimit: 300,
minStorageLimit: 0,
minFees: 0,
minEstimatedFees: 1000,
}
const unrevealedAccount = {
address: 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V',
balance: BigInt(1000000),
revealed: false,
xpub: '021bab48f41fc555e0fcf322a28e31b56f4369242f65324758ec8bbae3e84109a5',
}
const revealedAccount = {
...unrevealedAccount,
revealed: true,
}
function setupFa2ContractMock() {
const toTransferParams = jest.fn().mockReturnValue({
parameter: { entrypoint: 'transfer', value: [] },
amount: 0,
mutez: true,
})
const transfer = jest.fn().mockReturnValue({ toTransferParams })
return { transfer, toTransferParams }
}
describe('estimateFees', () => {
const mockTezosToolkit = {
estimate: {
finalizeUnstake: jest.fn(),
setDelegate: jest.fn(),
stake: jest.fn(),
transfer: jest.fn(),
unstake: jest.fn(),
},
contract: {
at: jest.fn(),
},
setProvider: jest.fn(),
}
let currentConfig: TezosCoinConfig
const context: TezosContext = {
config: async () => currentConfig,
logger: () => undefined,
}
const mkConfig = (cfg: unknown): TezosCoinConfig => cfg as unknown as TezosCoinConfig
beforeEach(() => {
jest.clearAllMocks()
;(getTezosToolkit as jest.Mock).mockReturnValue(mockTezosToolkit)
currentConfig = {
fees: defaultFeesConfig,
} as unknown as TezosCoinConfig
})
describe('signer public key from account.xpub', () => {
it('builds the mock signer from the account public key so reveal estimation is enabled', async () => {
// xpub holds the account public key (hex or base58; estimateFees normalizes it).
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: 100,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
await estimateFees(context, {
account: { ...unrevealedAccount },
transaction: {
mode: 'delegate',
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
})
const signer = mockTezosToolkit.setProvider.mock.calls.at(-1)?.[0]?.signer
// Signer carries the account's public key (normalized from xpub) so the reveal fee is
// accounted for; an empty signer key would mean reveal estimation is skipped.
const expectedPublicKey = normalizePublicKeyForAddress(
unrevealedAccount.xpub,
unrevealedAccount.address
)
expect(expectedPublicKey?.length).toBeGreaterThan(0)
await expect(signer.publicKey()).resolves.toBe(expectedPublicKey)
})
})
describe('when suggested fee is above or below minFees floor', () => {
it('estimate fees when default estimation is greater than minFees', async () => {
const suggestedFee = 2000
const minFees = 1000
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'delegate',
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
})
expect(result.fees).toBe(BigInt(suggestedFee))
expect(result.estimatedFees).toBe(BigInt(suggestedFee))
})
it('estimate fees when default estimation is lesser than minFees', async () => {
const minFees = 1000
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: 100,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'delegate',
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
})
expect(result.fees).toBe(BigInt(minFees))
expect(result.estimatedFees).toBe(BigInt(minFees))
})
it('estimate fees when default estimation is lesser than minFees (unrevealed composite)', async () => {
const minFees = 1000
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: 100,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const account = { ...unrevealedAccount }
const transaction = {
mode: 'delegate' as const,
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
}
const result = await estimateFees(context, { account, transaction })
expect(result.fees).toBe(BigInt(minFees))
expect(result.estimatedFees).toBe(BigInt(minFees * 2))
})
})
it('uses getRevealFee for unrevealed account when minFees is 0', async () => {
const delegationFee = 500
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: delegationFee,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees: 0, minEstimatedFees: 0 },
})
const account = { ...unrevealedAccount }
const transaction = {
mode: 'delegate' as const,
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
}
const result = await estimateFees(context, { account, transaction })
const expectedRevealFee = getRevealFee(account.address)
expect(result.estimatedFees).toBe(BigInt(delegationFee + expectedRevealFee))
})
it('undelegate uses same fee logic as delegate', async () => {
const suggestedFee = 800
mockTezosToolkit.estimate.setDelegate.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 10000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'undelegate',
recipient: '',
amount: BigInt(0),
},
})
expect(result.fees).toBe(BigInt(suggestedFee))
expect(result.estimatedFees).toBe(BigInt(suggestedFee))
expect(mockTezosToolkit.estimate.setDelegate).toHaveBeenCalledWith({
source: revealedAccount.address,
})
})
it('stake uses Taquito stake estimation and maps fee fields', async () => {
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: 700,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'stake',
recipient: '',
amount: 1234n,
},
})
expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
amount: 1234,
mutez: true,
})
expect(result.fees).toBe(700n)
expect(result.gasLimit).toBe(1100n)
expect(result.storageLimit).toBe(5n)
expect(result.estimatedFees).toBe(700n)
expect(result.amount).toBe(1234n)
})
it('unstake uses Taquito unstake estimation and maps fee fields', async () => {
mockTezosToolkit.estimate.unstake.mockResolvedValue({
suggestedFeeMutez: 710,
gasLimit: 1200,
storageLimit: 6,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'unstake',
recipient: '',
amount: 4321n,
},
})
expect(mockTezosToolkit.estimate.unstake).toHaveBeenCalledWith({
amount: 4321,
mutez: true,
})
expect(result.fees).toBe(710n)
expect(result.gasLimit).toBe(1200n)
expect(result.storageLimit).toBe(6n)
expect(result.estimatedFees).toBe(710n)
expect(result.amount).toBe(4321n)
})
it('useAllAmount unstake resolves max to the staked balance', async () => {
mockTezosToolkit.estimate.unstake.mockResolvedValue({
suggestedFeeMutez: 710,
gasLimit: 1200,
storageLimit: 6,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...revealedAccount, stakedBalance: 5000n },
transaction: {
mode: 'unstake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(result.amount).toBe(5000n)
})
it('useAllAmount stake coerces amount to 1 for estimation and resolves max to balance minus fees and reserve', async () => {
const suggestedFee = 700
const balance = 1_000_000n
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance },
transaction: {
mode: 'stake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
amount: 1,
mutez: true,
})
expect(result.fees).toBe(BigInt(suggestedFee))
expect(result.amount).toBe(balance - BigInt(suggestedFee) - 10_000n)
})
it('useAllAmount stake subtracts reveal fee from maxStakable on unrevealed accounts', async () => {
const suggestedFee = 700
const balance = 1_000_000n
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...unrevealedAccount, balance },
transaction: {
mode: 'stake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
const expectedRevealFee = BigInt(getRevealFee(unrevealedAccount.address))
expect(result.amount).toBe(balance - BigInt(suggestedFee) - expectedRevealFee - 10_000n)
expect(result.amount! + result.estimatedFees).toBeLessThanOrEqual(balance)
})
it('useAllAmount stake clamps amount to 0 when balance cannot cover fees and reserve', async () => {
const suggestedFee = 700
const balance = 5_000n
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance },
transaction: {
mode: 'stake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(result.amount).toBe(0n)
})
it('useAllAmount stake excludes already-staked funds from maxStakable', async () => {
const suggestedFee = 700
const balance = 1_000_000n
const stakedBalance = 300_000n
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance, stakedBalance },
transaction: {
mode: 'stake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(result.amount).toBe(
balance - stakedBalance - BigInt(suggestedFee) - STAKE_USE_ALL_RESERVE_MUTEZ
)
})
it('useAllAmount stake excludes unstaked-frozen funds from maxStakable', async () => {
const suggestedFee = 700
const balance = 1_000_000n
const stakedBalance = 300_000n
const unstakedBalance = 200_000n
mockTezosToolkit.estimate.stake.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1100,
storageLimit: 5,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance, stakedBalance, unstakedBalance },
transaction: {
mode: 'stake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(result.amount).toBe(
balance - stakedBalance - unstakedBalance - BigInt(suggestedFee) - STAKE_USE_ALL_RESERVE_MUTEZ
)
})
it('useAllAmount unstake coerces amount to 1 for Taquito estimation', async () => {
mockTezosToolkit.estimate.unstake.mockResolvedValue({
suggestedFeeMutez: 710,
gasLimit: 1200,
storageLimit: 6,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'unstake',
recipient: '',
amount: 0n,
useAllAmount: true,
},
})
expect(mockTezosToolkit.estimate.unstake).toHaveBeenCalledWith({
amount: 1,
mutez: true,
})
expect(result.fees).toBe(710n)
expect(result.amount).toBe(0n)
})
it('finalize_unstake uses Taquito finalizeUnstake estimation and maps fee fields', async () => {
mockTezosToolkit.estimate.finalizeUnstake.mockResolvedValue({
suggestedFeeMutez: 720,
gasLimit: 1300,
storageLimit: 7,
burnFeeMutez: 0,
opSize: 100,
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'finalize_unstake',
recipient: '',
amount: 0n,
},
})
expect(mockTezosToolkit.estimate.finalizeUnstake).toHaveBeenCalledWith({})
expect(result.fees).toBe(720n)
expect(result.gasLimit).toBe(1300n)
expect(result.storageLimit).toBe(7n)
expect(result.estimatedFees).toBe(720n)
expect(result.amount).toBe(0n)
})
it('transfer (send) applies minFees and returns gas/storage from estimate', async () => {
const suggestedFee = 350
const minFees = 500
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: suggestedFee,
gasLimit: 1500,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 200,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(1000),
},
})
expect(result.fees).toBe(BigInt(minFees))
expect(result.estimatedFees).toBe(BigInt(minFees))
expect(result.gasLimit).toBe(1500n)
expect(result.storageLimit).toBe(0n)
expect(result.amount).toBe(1000n)
})
it('transfer (send) unrevealed adds reveal fee to estimatedFees', async () => {
const transferFee = 400
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: transferFee,
gasLimit: 1500,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 200,
})
const result = await estimateFees(context, {
account: unrevealedAccount,
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(1000),
},
})
const expectedRevealFee = getRevealFee(unrevealedAccount.address)
expect(result.fees).toBe(BigInt(transferFee))
expect(result.estimatedFees).toBe(BigInt(transferFee + expectedRevealFee))
})
it('returns zero estimation when balance is 0 and not useAllAmount', async () => {
const result = await estimateFees(context, {
account: { ...revealedAccount, balance: 0n },
transaction: {
mode: 'delegate',
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
})
expect(result.fees).toBe(0n)
expect(result.estimatedFees).toBe(0n)
expect(mockTezosToolkit.estimate.setDelegate).not.toHaveBeenCalled()
})
it('returns estimation with amount 0 when balance is 0 and useAllAmount', async () => {
const result = await estimateFees(context, {
account: { ...revealedAccount, balance: 0n },
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(0),
useAllAmount: true,
},
})
expect(result.amount).toBe(0n)
expect(mockTezosToolkit.estimate.transfer).not.toHaveBeenCalled()
})
it('returns fallback estimation when send has no recipient', async () => {
currentConfig = mkConfig({
fees: {
...defaultFeesConfig,
minEstimatedFees: 300,
minGasLimit: 600,
minStorageLimit: 0,
},
})
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send',
recipient: '',
amount: BigInt(100),
useAllAmount: false,
},
})
expect(result.fees).toBe(300n)
expect(result.estimatedFees).toBe(300n)
expect(result.gasLimit).toBe(600n)
expect(mockTezosToolkit.estimate.transfer).not.toHaveBeenCalled()
})
it('uses fallback estimation and adds reveal fee when Taquito throws Public key not found (unrevealed)', async () => {
const minEstimatedFees = 400
currentConfig = mkConfig({
fees: {
...defaultFeesConfig,
minEstimatedFees,
minGasLimit: 600,
minStorageLimit: 0,
},
})
mockTezosToolkit.estimate.transfer.mockRejectedValue(
Object.assign(new Error('Fail'), { status: 404, message: 'Public key not found' })
)
const result = await estimateFees(context, {
account: unrevealedAccount,
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(100),
},
})
const expectedRevealFee = getRevealFee(unrevealedAccount.address)
expect(result.fees).toBe(BigInt(minEstimatedFees))
expect(result.estimatedFees).toBe(BigInt(minEstimatedFees + expectedRevealFee))
})
it('send_token (FA2) revealed applies estimation from contract transfer', async () => {
const { transfer } = setupFa2ContractMock()
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: 600,
gasLimit: 5000,
storageLimit: 10,
burnFeeMutez: 0,
opSize: 250,
})
const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send_token',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(100),
contractAddress,
tokenId: 0,
},
})
expect(mockTezosToolkit.contract.at).toHaveBeenCalledWith(contractAddress)
expect(transfer).toHaveBeenCalledWith([
{
from_: revealedAccount.address,
txs: [{ to_: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr', token_id: 0, amount: 100n }],
},
])
expect(result.fees).toBe(600n)
expect(result.gasLimit).toBe(5000n)
expect(result.storageLimit).toBe(10n)
expect(result.estimatedFees).toBe(600n)
expect(result.amount).toBe(100n)
})
it('send_token unrevealed adds reveal fee to estimatedFees', async () => {
const { transfer } = setupFa2ContractMock()
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: 400,
gasLimit: 4000,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 200,
})
const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
const result = await estimateFees(context, {
account: unrevealedAccount,
transaction: {
mode: 'send_token',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(50),
contractAddress,
tokenId: 3,
},
})
const expectedRevealFee = getRevealFee(unrevealedAccount.address)
expect(result.fees).toBe(400n)
expect(result.estimatedFees).toBe(BigInt(400 + expectedRevealFee))
})
it('send_token uses fallback when Taquito throws Public key not found', async () => {
const minEstimatedFees = 400
currentConfig = mkConfig({
fees: {
...defaultFeesConfig,
minEstimatedFees,
minGasLimit: 600,
minStorageLimit: 0,
},
})
const { transfer } = setupFa2ContractMock()
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
mockTezosToolkit.estimate.transfer.mockRejectedValue(
Object.assign(new Error('Fail'), { status: 404, message: 'Public key not found' })
)
const contractAddress = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
const result = await estimateFees(context, {
account: unrevealedAccount,
transaction: {
mode: 'send_token',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(10),
contractAddress,
tokenId: 0,
},
})
const expectedRevealFee = getRevealFee(unrevealedAccount.address)
expect(result.fees).toBe(BigInt(minEstimatedFees))
expect(result.estimatedFees).toBe(BigInt(minEstimatedFees + expectedRevealFee))
expect(result.amount).toBe(10n)
})
it('send_token with empty recipient returns fallback without calling the contract', async () => {
const { transfer } = setupFa2ContractMock()
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
const result = await estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send_token',
recipient: '',
amount: BigInt(100),
contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
tokenId: 0,
},
})
expect(mockTezosToolkit.contract.at).not.toHaveBeenCalled()
expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
expect(transfer).not.toHaveBeenCalled()
})
it('send_token propagates error when contract.at fails', async () => {
mockTezosToolkit.contract.at.mockRejectedValue(new Error('Invalid contract'))
await expect(
estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send_token',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(1),
contractAddress: 'KT1Bad',
tokenId: 0,
},
})
).rejects.toThrow('Invalid contract')
})
it('send_token throws when contractAddress or tokenId is missing', async () => {
await expect(
estimateFees(context, {
account: revealedAccount,
transaction: {
mode: 'send_token',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(1),
tokenId: 0,
},
})
).rejects.toThrow('FA2 transfer requires contractAddress and tokenId')
})
it('useAllAmount send computes max amount and applies minFees to main op', async () => {
const minFees = 500
const balance = 10000
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: 100,
gasLimit: 1500,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 200,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance: BigInt(balance) },
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: BigInt(0),
useAllAmount: true,
},
})
expect(result.fees).toBe(BigInt(minFees))
// max = spendable(10000) - mainOpFee(500) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
// = 9500 - (500 - 250) = 9250
expect(result.amount).toBe(9250n)
})
// Regression for LIVE-28506: the max must reserve the fee that is actually reported, so that
// `amount + estimatedFees` always fits the spendable balance. Reserving the raw taquito
// suggestion instead made every send-max fail validation once `minFees` exceeded it by more
// than the dust margin — at any balance.
it.each([
// balance, minFees, suggestedFeeMutez, burnFeeMutez, expected amount
[10000n, 2000, 100, 0, 7750n],
[10000n, 100, 2000, 0, 7750n],
// recipient allocation burn (277 bytes * COST_PER_BYTE), minus taquito's 20-byte pad
[100000n, 2000, 100, 69250, 33500n],
])(
'useAllAmount send reserves the reported fee (balance=%i, minFees=%i, suggested=%i, burn=%i)',
async (balance, minFees, suggestedFeeMutez, burnFeeMutez, expectedAmount) => {
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez,
gasLimit: 1500,
storageLimit: 0,
burnFeeMutez,
opSize: 200,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees },
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance },
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: 0n,
useAllAmount: true,
},
})
expect(result.amount).toBe(expectedAmount)
// the invariant validateIntent's balance-coverage check relies on
expect(result.amount! + result.estimatedFees).toBeLessThanOrEqual(balance)
}
)
it('useAllAmount send excludes staked and unstaked funds from the max', async () => {
const balance = 10000n
const stakedBalance = 3000n
const unstakedBalance = 1000n
mockTezosToolkit.estimate.transfer.mockResolvedValue({
suggestedFeeMutez: 100,
gasLimit: 1500,
storageLimit: 0,
burnFeeMutez: 0,
opSize: 200,
})
currentConfig = mkConfig({
fees: { ...defaultFeesConfig, minFees: 0 },
})
const result = await estimateFees(context, {
account: { ...revealedAccount, balance, stakedBalance, unstakedBalance },
transaction: {
mode: 'send',
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
amount: 0n,
useAllAmount: true,
},
})
// spendable = balance(10000) - staked(3000) - unstaked(1000) = 6000
// max = 6000 - suggestedFee(100) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
// = 5900 - (500 - 250) = 5650
expect(result.amount).toBe(5650n)
})
})