Skip to content

Commit 9a32a74

Browse files
committed
fix(coin-tezos): reserve the reported fee when computing send-max
1 parent 16e4819 commit 9a32a74

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

.changeset/lucky-otters-wander.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/coin-tezos": minor
3+
---
4+
5+
Fix Tezos send-max reporting insufficient funds when the minFees floor exceeds the estimated fee

libs/coin-modules/coin-tezos/src/logic/estimateFees.test.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -812,11 +812,51 @@ describe("estimateFees", () => {
812812
});
813813

814814
expect(result.fees).toBe(BigInt(minFees));
815-
// max = spendable(10000) - suggestedFee(100) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
816-
// = 9900 - (500 - 250) = 9650
817-
expect(result.amount).toBe(9650n);
815+
// max = spendable(10000) - mainOpFee(500) - (DUST_MARGIN(500) - (DUST_MARGIN*0.1 + opSize(200)))
816+
// = 9500 - (500 - 250) = 9250
817+
expect(result.amount).toBe(9250n);
818818
});
819819

820+
// Regression for LIVE-28506: the max must reserve the fee that is actually reported, so that
821+
// `amount + estimatedFees` always fits the spendable balance. Reserving the raw taquito
822+
// suggestion instead made every send-max fail validation once `minFees` exceeded it by more
823+
// than the dust margin — at any balance.
824+
it.each([
825+
// balance, minFees, suggestedFeeMutez, burnFeeMutez, expected amount
826+
[10000n, 2000, 100, 0, 7750n],
827+
[10000n, 100, 2000, 0, 7750n],
828+
// recipient allocation burn (277 bytes * COST_PER_BYTE), minus taquito's 20-byte pad
829+
[100000n, 2000, 100, 69250, 33500n],
830+
])(
831+
"useAllAmount send reserves the reported fee (balance=%i, minFees=%i, suggested=%i, burn=%i)",
832+
async (balance, minFees, suggestedFeeMutez, burnFeeMutez, expectedAmount) => {
833+
mockTezosToolkit.estimate.transfer.mockResolvedValue({
834+
suggestedFeeMutez,
835+
gasLimit: 1500,
836+
storageLimit: 0,
837+
burnFeeMutez,
838+
opSize: 200,
839+
});
840+
(coinConfig.getCoinConfig as jest.Mock).mockReturnValue({
841+
fees: { ...defaultFeesConfig, minFees },
842+
});
843+
844+
const result = await estimateFees({
845+
account: { ...revealedAccount, balance },
846+
transaction: {
847+
mode: "send",
848+
recipient: "tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr",
849+
amount: 0n,
850+
useAllAmount: true,
851+
},
852+
});
853+
854+
expect(result.amount).toBe(expectedAmount);
855+
// the invariant validateIntent's balance-coverage check relies on
856+
expect(result.amount! + result.estimatedFees).toBeLessThanOrEqual(balance);
857+
},
858+
);
859+
820860
it("useAllAmount send excludes staked and unstaked funds from the max", async () => {
821861
const balance = 10000n;
822862
const stakedBalance = 3000n;

libs/coin-modules/coin-tezos/src/logic/estimateFees.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,15 @@ export async function estimateFees({
183183

184184
// NOTE: send-max only applies to native XTZ transfer, not FA2
185185
if (transaction.useAllAmount && transaction.mode === "send") {
186+
// Reserve `mainOpFee`, not the raw taquito suggestion: `estimatedFees` below reports the
187+
// `minFees` floor, so subtracting the (lower) suggestion here yields an amount whose
188+
// `amount + estimatedFees` exceeds the spendable balance, and validateIntent then rejects
189+
// the very max it was handed (LIVE-28506).
186190
// NOTE: from https://github.com/ecadlabs/taquito/blob/master/integration-tests/__tests__/contract/empty-implicit-account-into-new-implicit-account.spec.ts#L37
187191
const totalFees =
188192
estimate.burnFeeMutez > 0
189-
? estimate.suggestedFeeMutez + estimate.burnFeeMutez - 20 * COST_PER_BYTE // 20 is storage buffer
190-
: estimate.suggestedFeeMutez;
193+
? mainOpFee + estimate.burnFeeMutez - 20 * COST_PER_BYTE // 20 is storage buffer
194+
: mainOpFee;
191195
const maxAmount = spendableForMax - (totalFees + Number(revealFee));
192196
// NOTE: from https://github.com/ecadlabs/taquito/blob/a70c64c4b105381bb9f1d04c9c70e8ef26e9241c/integration-tests/contract-empty-implicit-account-into-new-implicit-account.spec.ts#L33
193197
// Temporary fix, see https://gitlab.com/tezos/tezos/-/issues/1754
@@ -196,6 +200,14 @@ export async function estimateFees({
196200
const incr = DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize);
197201
const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr);
198202
estimation.amount = maxMinusBuff > 0 ? BigInt(maxMinusBuff) : 0n;
203+
log("tezos-send-max", "send-max fee inputs", {
204+
minFees,
205+
mainOpFee,
206+
suggestedFeeMutez: estimate.suggestedFeeMutez,
207+
burnFeeMutez: estimate.burnFeeMutez,
208+
opSize: Number(estimate.opSize),
209+
revealFee: Number(revealFee),
210+
});
199211
} else if (transaction.useAllAmount && transaction.mode === "stake") {
200212
estimation.amount = computeMaxStakeAmount(
201213
BigInt(account.balance),

0 commit comments

Comments
 (0)