@ledgerhq/coin-tezos 10.2.2 → 10.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/lib/api/index.d.ts +1 -3
  3. package/lib/api/index.d.ts.map +1 -1
  4. package/lib/api/index.js +49 -26
  5. package/lib/api/index.js.map +1 -1
  6. package/lib/logic/craftRawOperations.js +3 -4
  7. package/lib/logic/craftRawOperations.js.map +1 -1
  8. package/lib/logic/craftTransaction.d.ts +6 -0
  9. package/lib/logic/craftTransaction.d.ts.map +1 -1
  10. package/lib/logic/craftTransaction.js +2 -3
  11. package/lib/logic/craftTransaction.js.map +1 -1
  12. package/lib/logic/estimateFees.d.ts +7 -1
  13. package/lib/logic/estimateFees.d.ts.map +1 -1
  14. package/lib/logic/estimateFees.js +81 -6
  15. package/lib/logic/estimateFees.js.map +1 -1
  16. package/lib/logic/estimateRevealLimits.d.ts +2 -2
  17. package/lib/logic/estimateRevealLimits.d.ts.map +1 -1
  18. package/lib/logic/estimateRevealLimits.js +3 -3
  19. package/lib/logic/estimateRevealLimits.js.map +1 -1
  20. package/lib/logic/getBalance.d.ts.map +1 -1
  21. package/lib/logic/getBalance.js +2 -3
  22. package/lib/logic/getBalance.js.map +1 -1
  23. package/lib/logic/getBlock.js +6 -6
  24. package/lib/logic/getBlock.js.map +1 -1
  25. package/lib/logic/getStakes.d.ts +2 -1
  26. package/lib/logic/getStakes.d.ts.map +1 -1
  27. package/lib/logic/getStakes.js +8 -9
  28. package/lib/logic/getStakes.js.map +1 -1
  29. package/lib/logic/listOperations.d.ts.map +1 -1
  30. package/lib/logic/listOperations.js +16 -16
  31. package/lib/logic/listOperations.js.map +1 -1
  32. package/lib/logic/validateIntent.d.ts.map +1 -1
  33. package/lib/logic/validateIntent.js +14 -2
  34. package/lib/logic/validateIntent.js.map +1 -1
  35. package/lib/network/bakers.d.ts +6 -5
  36. package/lib/network/bakers.d.ts.map +1 -1
  37. package/lib/network/bakers.js +13 -12
  38. package/lib/network/bakers.js.map +1 -1
  39. package/lib/network/tzkt.d.ts +8 -8
  40. package/lib/network/tzkt.d.ts.map +1 -1
  41. package/lib/network/tzkt.js +13 -11
  42. package/lib/network/tzkt.js.map +1 -1
  43. package/lib/types/errors.d.ts +3 -0
  44. package/lib/types/errors.d.ts.map +1 -1
  45. package/lib/types/errors.js +8 -1
  46. package/lib/types/errors.js.map +1 -1
  47. package/lib/utils.d.ts +17 -0
  48. package/lib/utils.d.ts.map +1 -1
  49. package/lib/utils.js +31 -1
  50. package/lib/utils.js.map +1 -1
  51. package/lib-es/api/index.d.ts +1 -3
  52. package/lib-es/api/index.d.ts.map +1 -1
  53. package/lib-es/api/index.js +50 -27
  54. package/lib-es/api/index.js.map +1 -1
  55. package/lib-es/logic/craftRawOperations.js +3 -4
  56. package/lib-es/logic/craftRawOperations.js.map +1 -1
  57. package/lib-es/logic/craftTransaction.d.ts +6 -0
  58. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  59. package/lib-es/logic/craftTransaction.js +2 -3
  60. package/lib-es/logic/craftTransaction.js.map +1 -1
  61. package/lib-es/logic/estimateFees.d.ts +7 -1
  62. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  63. package/lib-es/logic/estimateFees.js +82 -8
  64. package/lib-es/logic/estimateFees.js.map +1 -1
  65. package/lib-es/logic/estimateRevealLimits.d.ts +2 -2
  66. package/lib-es/logic/estimateRevealLimits.d.ts.map +1 -1
  67. package/lib-es/logic/estimateRevealLimits.js +3 -3
  68. package/lib-es/logic/estimateRevealLimits.js.map +1 -1
  69. package/lib-es/logic/getBalance.d.ts.map +1 -1
  70. package/lib-es/logic/getBalance.js +2 -3
  71. package/lib-es/logic/getBalance.js.map +1 -1
  72. package/lib-es/logic/getBlock.js +6 -6
  73. package/lib-es/logic/getBlock.js.map +1 -1
  74. package/lib-es/logic/getStakes.d.ts +2 -1
  75. package/lib-es/logic/getStakes.d.ts.map +1 -1
  76. package/lib-es/logic/getStakes.js +8 -9
  77. package/lib-es/logic/getStakes.js.map +1 -1
  78. package/lib-es/logic/listOperations.d.ts.map +1 -1
  79. package/lib-es/logic/listOperations.js +16 -16
  80. package/lib-es/logic/listOperations.js.map +1 -1
  81. package/lib-es/logic/validateIntent.d.ts.map +1 -1
  82. package/lib-es/logic/validateIntent.js +16 -4
  83. package/lib-es/logic/validateIntent.js.map +1 -1
  84. package/lib-es/network/bakers.d.ts +6 -5
  85. package/lib-es/network/bakers.d.ts.map +1 -1
  86. package/lib-es/network/bakers.js +13 -12
  87. package/lib-es/network/bakers.js.map +1 -1
  88. package/lib-es/network/tzkt.d.ts +8 -8
  89. package/lib-es/network/tzkt.d.ts.map +1 -1
  90. package/lib-es/network/tzkt.js +13 -11
  91. package/lib-es/network/tzkt.js.map +1 -1
  92. package/lib-es/types/errors.d.ts +3 -0
  93. package/lib-es/types/errors.d.ts.map +1 -1
  94. package/lib-es/types/errors.js +6 -0
  95. package/lib-es/types/errors.js.map +1 -1
  96. package/lib-es/utils.d.ts +17 -0
  97. package/lib-es/utils.d.ts.map +1 -1
  98. package/lib-es/utils.js +29 -0
  99. package/lib-es/utils.js.map +1 -1
  100. package/package.json +3 -4
  101. package/src/api/index.test.ts +208 -0
  102. package/src/api/index.ts +55 -28
  103. package/src/logic/craftRawOperations.ts +3 -4
  104. package/src/logic/craftTransaction.test.ts +30 -0
  105. package/src/logic/craftTransaction.ts +8 -3
  106. package/src/logic/estimateFees.test.ts +224 -2
  107. package/src/logic/estimateFees.ts +92 -7
  108. package/src/logic/estimateRevealLimits.ts +11 -7
  109. package/src/logic/getBalance.ts +9 -6
  110. package/src/logic/getBlock.ts +6 -6
  111. package/src/logic/getStakes.ts +13 -8
  112. package/src/logic/listOperations.ts +23 -13
  113. package/src/logic/validateIntent.test.ts +90 -1
  114. package/src/logic/validateIntent.ts +18 -2
  115. package/src/network/bakers.integ.test.ts +3 -2
  116. package/src/network/bakers.test.ts +2 -1
  117. package/src/network/bakers.ts +15 -13
  118. package/src/network/tzkt.test.ts +29 -30
  119. package/src/network/tzkt.ts +38 -23
  120. package/src/types/errors.ts +7 -0
  121. package/src/utils.ts +32 -0
@@ -54,9 +54,19 @@ describe('estimateFees', () => {
54
54
  contract: {
55
55
  at: jest.fn(),
56
56
  },
57
+ rpc: {
58
+ getConstants: jest.fn(),
59
+ },
57
60
  setProvider: jest.fn(),
58
61
  }
59
62
 
63
+ // Mainnet values; the two are equal, which is what makes a reveal + operation batch
64
+ // overflow the block limit unless the operation's gas is capped.
65
+ const MAINNET_GAS_CONSTANTS = {
66
+ hard_gas_limit_per_operation: 1040000,
67
+ hard_gas_limit_per_block: 1040000,
68
+ }
69
+
60
70
  let currentConfig: TezosCoinConfig
61
71
  const context: TezosContext = {
62
72
  config: async () => currentConfig,
@@ -72,6 +82,7 @@ describe('estimateFees', () => {
72
82
  jest
73
83
  .mocked<DeepPartialReturn<typeof getTezosToolkit>>(getTezosToolkit)
74
84
  .mockReturnValue(mockTezosToolkit)
85
+ mockTezosToolkit.rpc.getConstants.mockResolvedValue(MAINNET_GAS_CONSTANTS)
75
86
  currentConfig = mkConfig(defaultFeesConfig)
76
87
  })
77
88
 
@@ -312,7 +323,7 @@ describe('estimateFees', () => {
312
323
  expect(result.amount).toBe(5000n)
313
324
  })
314
325
 
315
- it('useAllAmount stake coerces amount to 1 for estimation and resolves max to balance minus fees and reserve', async () => {
326
+ it('useAllAmount stake probes above the pseudotoken rate and resolves max to balance minus fees and reserve', async () => {
316
327
  const suggestedFee = 700
317
328
  const balance = 1_000_000n
318
329
  mockTezosToolkit.estimate.stake.mockResolvedValue({
@@ -334,13 +345,48 @@ describe('estimateFees', () => {
334
345
  })
335
346
 
336
347
  expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
337
- amount: 1,
348
+ amount: 1000,
338
349
  mutez: true,
339
350
  })
340
351
  expect(result.fees).toBe(BigInt(suggestedFee))
341
352
  expect(result.amount).toBe(balance - BigInt(suggestedFee) - 10_000n)
342
353
  })
343
354
 
355
+ it('stake with no amount yet returns the fallback estimation without simulating', async () => {
356
+ const result = await estimateFees(context, {
357
+ account: revealedAccount,
358
+ transaction: {
359
+ mode: 'stake',
360
+ recipient: '',
361
+ amount: 0n,
362
+ },
363
+ })
364
+
365
+ expect(mockTezosToolkit.estimate.stake).not.toHaveBeenCalled()
366
+ expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
367
+ expect(result.estimatedFees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
368
+ expect(result.gasLimit).toBe(BigInt(defaultFeesConfig.minGasLimit))
369
+ expect(result.storageLimit).toBe(BigInt(defaultFeesConfig.minStorageLimit))
370
+ })
371
+
372
+ it('stake with no amount yet adds the reveal fee to the fallback on unrevealed accounts', async () => {
373
+ const result = await estimateFees(context, {
374
+ account: unrevealedAccount,
375
+ transaction: {
376
+ mode: 'stake',
377
+ recipient: '',
378
+ amount: 0n,
379
+ },
380
+ })
381
+
382
+ expect(mockTezosToolkit.estimate.stake).not.toHaveBeenCalled()
383
+ const expectedRevealFee = BigInt(getRevealFee(unrevealedAccount.address))
384
+ expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
385
+ expect(result.estimatedFees).toBe(
386
+ BigInt(defaultFeesConfig.minEstimatedFees) + expectedRevealFee
387
+ )
388
+ })
389
+
344
390
  it('useAllAmount stake subtracts reveal fee from maxStakable on unrevealed accounts', async () => {
345
391
  const suggestedFee = 700
346
392
  const balance = 1_000_000n
@@ -620,6 +666,93 @@ describe('estimateFees', () => {
620
666
  expect(result.estimatedFees).toBe(BigInt(400 + expectedRevealFee))
621
667
  })
622
668
 
669
+ it('reports a validation-time protocol error as taquitoError instead of throwing', async () => {
670
+ const stakeAmountTooSmall = 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small'
671
+ mockTezosToolkit.estimate.stake.mockRejectedValue(
672
+ Object.assign(new Error('Http error response: (500)'), {
673
+ status: 500,
674
+ body: `[{"kind":"permanent","id":"${stakeAmountTooSmall}","amount":"1"}]`,
675
+ })
676
+ )
677
+
678
+ const result = await estimateFees(context, {
679
+ account: revealedAccount,
680
+ transaction: {
681
+ mode: 'stake',
682
+ recipient: '',
683
+ amount: 1n,
684
+ },
685
+ })
686
+
687
+ expect(result.taquitoError).toBe(stakeAmountTooSmall)
688
+ expect(result.fees).toBe(0n)
689
+ })
690
+
691
+ it('picks the mapped id out of a multi-error trace, not the generic one leading it', async () => {
692
+ const scriptRejected = 'proto.025-PsUshuai.michelson_v1.script_rejected'
693
+ mockTezosToolkit.estimate.transfer.mockRejectedValue(
694
+ Object.assign(new Error('Http error response: (500)'), {
695
+ status: 500,
696
+ body: JSON.stringify([
697
+ { kind: 'temporary', id: 'proto.025-PsUshuai.michelson_v1.runtime_error' },
698
+ { kind: 'temporary', id: scriptRejected, with: { string: 'nope' } },
699
+ ]),
700
+ })
701
+ )
702
+
703
+ const result = await estimateFees(context, {
704
+ account: revealedAccount,
705
+ transaction: {
706
+ mode: 'send',
707
+ recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
708
+ amount: 1000n,
709
+ },
710
+ })
711
+
712
+ expect(result.taquitoError).toBe(scriptRejected)
713
+ })
714
+
715
+ it('still finds the id when the body is not valid JSON', async () => {
716
+ const stakeAmountTooSmall = 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small'
717
+ mockTezosToolkit.estimate.stake.mockRejectedValue(
718
+ Object.assign(new Error('Http error response: (500)'), {
719
+ status: 500,
720
+ body: `[{"kind":"permanent","id":"${stakeAmountTooSmall}","amount":"1"`,
721
+ })
722
+ )
723
+
724
+ const result = await estimateFees(context, {
725
+ account: revealedAccount,
726
+ transaction: {
727
+ mode: 'stake',
728
+ recipient: '',
729
+ amount: 1n,
730
+ },
731
+ })
732
+
733
+ expect(result.taquitoError).toBe(stakeAmountTooSmall)
734
+ })
735
+
736
+ it('rethrows a protocol error the mapping does not cover, so callers can still retry', async () => {
737
+ mockTezosToolkit.estimate.setDelegate.mockRejectedValue(
738
+ Object.assign(new Error('Http error response: (500)'), {
739
+ status: 500,
740
+ body: '[{"kind":"permanent","id":"proto.025-PsUshuai.gas_limit_too_high"}]',
741
+ })
742
+ )
743
+
744
+ await expect(
745
+ estimateFees(context, {
746
+ account: revealedAccount,
747
+ transaction: {
748
+ mode: 'delegate',
749
+ recipient: 'tz1validator',
750
+ amount: 0n,
751
+ },
752
+ })
753
+ ).rejects.toThrow('Http error response: (500)')
754
+ })
755
+
623
756
  it('send_token (FA2) revealed applies estimation from contract transfer', async () => {
624
757
  const { transfer } = setupFa2ContractMock()
625
758
  mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
@@ -657,6 +790,95 @@ describe('estimateFees', () => {
657
790
  expect(result.amount).toBe(100n)
658
791
  })
659
792
 
793
+ it('send_token unrevealed caps gas so the reveal batch fits the block limit', async () => {
794
+ const { transfer } = setupFa2ContractMock()
795
+ mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
796
+ mockTezosToolkit.estimate.transfer.mockResolvedValue({
797
+ suggestedFeeMutez: 600,
798
+ gasLimit: 4000,
799
+ storageLimit: 0,
800
+ burnFeeMutez: 0,
801
+ opSize: 250,
802
+ })
803
+
804
+ await estimateFees(context, {
805
+ account: unrevealedAccount,
806
+ transaction: {
807
+ mode: 'send_token',
808
+ recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
809
+ amount: BigInt(100),
810
+ contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
811
+ tokenId: 0,
812
+ },
813
+ })
814
+
815
+ // min(1040000, 1040000 / 3): leaves room for the reveal Taquito prepends, so the
816
+ // simulation does not overflow hard_gas_limit_per_block.
817
+ expect(mockTezosToolkit.estimate.transfer).toHaveBeenCalledWith(
818
+ expect.objectContaining({ gasLimit: 346666 })
819
+ )
820
+ })
821
+
822
+ it('send_token derives the gas cap from protocol constants', async () => {
823
+ mockTezosToolkit.rpc.getConstants.mockResolvedValue({
824
+ hard_gas_limit_per_operation: 900000,
825
+ hard_gas_limit_per_block: 2400000,
826
+ })
827
+ const { transfer } = setupFa2ContractMock()
828
+ mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
829
+ mockTezosToolkit.estimate.transfer.mockResolvedValue({
830
+ suggestedFeeMutez: 600,
831
+ gasLimit: 4000,
832
+ storageLimit: 0,
833
+ burnFeeMutez: 0,
834
+ opSize: 250,
835
+ })
836
+
837
+ await estimateFees(context, {
838
+ account: unrevealedAccount,
839
+ transaction: {
840
+ mode: 'send_token',
841
+ recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
842
+ amount: BigInt(100),
843
+ contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
844
+ tokenId: 0,
845
+ },
846
+ })
847
+
848
+ // min(900000, 2400000 / 3): the block-derived bound is the tighter one here.
849
+ expect(mockTezosToolkit.estimate.transfer).toHaveBeenCalledWith(
850
+ expect.objectContaining({ gasLimit: 800000 })
851
+ )
852
+ })
853
+
854
+ it('send_token revealed passes no gas cap', async () => {
855
+ const { transfer } = setupFa2ContractMock()
856
+ mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
857
+ mockTezosToolkit.estimate.transfer.mockResolvedValue({
858
+ suggestedFeeMutez: 600,
859
+ gasLimit: 4000,
860
+ storageLimit: 0,
861
+ burnFeeMutez: 0,
862
+ opSize: 250,
863
+ })
864
+
865
+ await estimateFees(context, {
866
+ account: revealedAccount,
867
+ transaction: {
868
+ mode: 'send_token',
869
+ recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
870
+ amount: BigInt(100),
871
+ contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
872
+ tokenId: 0,
873
+ },
874
+ })
875
+
876
+ // No reveal is prepended, so the batch cannot overflow and Taquito keeps its own default.
877
+ const params = mockTezosToolkit.estimate.transfer.mock.calls.at(-1)?.[0]
878
+ expect(params).not.toHaveProperty('gasLimit')
879
+ expect(mockTezosToolkit.rpc.getConstants).not.toHaveBeenCalled()
880
+ })
881
+
660
882
  it('send_token unrevealed adds reveal fee to estimatedFees', async () => {
661
883
  const { transfer } = setupFa2ContractMock()
662
884
  mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
@@ -4,7 +4,6 @@
4
4
  import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import type { TezosOperationMode } from '../types/model'
6
6
  import type { Estimate } from '@taquito/taquito'
7
- import { log } from '@ledgerhq/logs'
8
7
  import { COST_PER_BYTE, getRevealFee, ORIGINATION_SIZE } from '@taquito/taquito'
9
8
  import { validatePublicKey, ValidationResult } from '@taquito/utils'
10
9
  import { UnsupportedTransactionMode } from '../types/errors'
@@ -13,10 +12,12 @@ import {
13
12
  createFallbackEstimation,
14
13
  createMockSigner,
15
14
  DUST_MARGIN_MUTEZ,
15
+ isMappedTaquitoError,
16
16
  MIN_SUGGESTED_FEE_SMALL_TRANSFER,
17
17
  OP_SIZE_XTZ_TRANSFER,
18
18
  normalizePublicKeyForAddress,
19
19
  partitionNativeBalance,
20
+ STAKE_ESTIMATION_PROBE_MUTEZ,
20
21
  } from '../utils'
21
22
  import { getTezosToolkit } from './tezosToolkit'
22
23
 
@@ -106,7 +107,8 @@ export async function estimateFees(
106
107
  transaction.mode === 'unstake')) ||
107
108
  (amount === 0n && transaction.mode !== 'send_token')
108
109
  if (coerceMinAmountForEstimation) {
109
- amount = 1n // send/stake max or zero-amount pre-estimation (taquito refuses 0); not used for FA2 send_token
110
+ // send/stake max or zero-amount pre-estimation (taquito refuses 0); not used for FA2 send_token
111
+ amount = transaction.mode === 'stake' ? STAKE_ESTIMATION_PROBE_MUTEZ : 1n
110
112
  }
111
113
 
112
114
  try {
@@ -119,6 +121,19 @@ export async function estimateFees(
119
121
  ...createFallbackEstimation(config),
120
122
  }
121
123
  }
124
+ const isStakeWithNoAmountYet =
125
+ transaction.mode === 'stake' && transaction.amount === 0n && !transaction.useAllAmount
126
+ if (isStakeWithNoAmountYet) {
127
+ const fallback = createFallbackEstimation(config)
128
+ const revealFee = account.revealed
129
+ ? 0n
130
+ : BigInt(getRevealFeeForEstimation(config, account.address))
131
+ return {
132
+ ...estimation,
133
+ ...fallback,
134
+ estimatedFees: fallback.estimatedFees + revealFee,
135
+ }
136
+ }
122
137
  let estimate: Estimate
123
138
  switch (transaction.mode) {
124
139
  case 'send':
@@ -175,9 +190,13 @@ export async function estimateFees(
175
190
  },
176
191
  ])
177
192
  .toTransferParams({ mutez: true })
193
+ const revealBatchGasLimit = account.revealed
194
+ ? undefined
195
+ : await gasLimitForRevealBatch(tezosToolkit)
178
196
  estimate = await tezosToolkit.estimate.transfer({
179
197
  ...transferParams,
180
198
  source: account.address,
199
+ ...(revealBatchGasLimit !== undefined && { gasLimit: revealBatchGasLimit }),
181
200
  })
182
201
  break
183
202
  }
@@ -210,7 +229,7 @@ export async function estimateFees(
210
229
  const incr = DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize)
211
230
  const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr)
212
231
  estimation.amount = maxMinusBuff > 0 ? BigInt(maxMinusBuff) : 0n
213
- log('tezos-send-max', 'send-max fee inputs', {
232
+ context.logger('tezos-send-max', 'send-max fee inputs', {
214
233
  minFees,
215
234
  mainOpFee,
216
235
  suggestedFeeMutez: estimate.suggestedFeeMutez,
@@ -240,14 +259,15 @@ export async function estimateFees(
240
259
  if (typeof e !== 'object' || !e) throw e
241
260
  if ('id' in e) {
242
261
  estimation.taquitoError = String(e.id)
243
- log('taquito-error', 'taquito got error ' + String(e.id))
262
+ context.logger('taquito-error', 'taquito got error ' + String(e.id))
244
263
  } else if ('status' in e) {
245
264
  const errorMessage = 'message' in e ? String(e.message) : ''
265
+ const protocolErrorId = mappedProtocolErrorId(e)
246
266
  if (
247
267
  errorMessage.includes('Public key not found') ||
248
268
  errorMessage.includes('wallet or contract API')
249
269
  ) {
250
- log(
270
+ context.logger(
251
271
  'taquito-network-error',
252
272
  'Recipient address not found (new account), using default fees',
253
273
  {
@@ -297,8 +317,14 @@ export async function estimateFees(
297
317
  // preserve input amount in fallback for readability/tests
298
318
  estimation.amount = transaction.amount
299
319
  }
320
+ } else if (protocolErrorId) {
321
+ estimation.taquitoError = protocolErrorId
322
+ context.logger(
323
+ 'taquito-error',
324
+ 'node refused the operation before applying it ' + protocolErrorId
325
+ )
300
326
  } else {
301
- log('taquito-network-error', errorMessage, {
327
+ context.logger('taquito-network-error', errorMessage, {
302
328
  transaction: transaction,
303
329
  })
304
330
  throw e
@@ -352,7 +378,66 @@ export async function estimateFees(
352
378
  return estimation
353
379
  }
354
380
 
355
- function getRevealFeeForEstimation(config: TezosCoinConfig, address: string): number {
381
+ /**
382
+ * A protocol error raised while validating an operation comes back as an HTTP error whose body is
383
+ * the raw error array, so Taquito exposes no `id` of its own for `mapTaquitoErrors` to match.
384
+ *
385
+ * Only the ids that mapping covers are reported: any other has to keep propagating, because
386
+ * callers retry on a thrown estimation (see the without-public-key retry in `api/index.ts`).
387
+ */
388
+ function mappedProtocolErrorId(error: object): string | undefined {
389
+ const body = 'body' in error && typeof error.body === 'string' ? error.body : ''
390
+ const message = 'message' in error ? String(error.message) : ''
391
+ // A trace lists the generic error first and the specific one last, the convention Taquito's
392
+ // own `TezosOperationError.lastError` relies on.
393
+ return protocolErrorIds(body || message)
394
+ .filter(isMappedTaquitoError)
395
+ .at(-1)
396
+ }
397
+
398
+ function protocolErrorIds(text: string): string[] {
399
+ try {
400
+ const parsed: unknown = JSON.parse(text)
401
+ const errors: unknown[] = Array.isArray(parsed) ? parsed : [parsed]
402
+ return errors.flatMap((error) =>
403
+ typeof error === 'object' && error !== null && 'id' in error && typeof error.id === 'string'
404
+ ? [error.id]
405
+ : []
406
+ )
407
+ } catch {
408
+ return Array.from(text.matchAll(/"id"\s*:\s*"([^"]+)"/g), (match) => match[1])
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Gas limit to hand Taquito when the source account is unrevealed.
414
+ *
415
+ * Taquito prepends a reveal to any operation from an unrevealed account, then gives the
416
+ * operation itself `hard_gas_limit_per_operation` for the simulation. Mainnet sets
417
+ * `hard_gas_limit_per_block` to that same value, so the reveal's own gas tips the batch past
418
+ * the block limit and the node answers HTTP 500 `gas_exhausted.block`. Without a cap the
419
+ * estimation fails and the caller falls back to native-transfer constants, which are far too
420
+ * small for an FA2 transfer.
421
+ *
422
+ * Mirrors Taquito's own batch adjustment, `min(perOperation, perBlock / (opCount + 1))`, so
423
+ * the value tracks the protocol rather than a hardcoded figure.
424
+ */
425
+ async function gasLimitForRevealBatch(
426
+ tezosToolkit: ReturnType<typeof getTezosToolkit>
427
+ ): Promise<number> {
428
+ const constants = await tezosToolkit.rpc.getConstants()
429
+ const perOperation = Number(constants.hard_gas_limit_per_operation)
430
+ const perBlock = Number(constants.hard_gas_limit_per_block)
431
+ const REVEAL_PLUS_OPERATION = 2
432
+ return Math.floor(Math.min(perOperation, perBlock / (REVEAL_PLUS_OPERATION + 1)))
433
+ }
434
+
435
+ /**
436
+ * Reveal fee the estimation reports as part of the total, derived from Taquito's static per-curve
437
+ * table rather than a node estimate. The account is unrevealed and the public key is often absent
438
+ * at estimation time, so `estimate.reveal()` is not reliably available here.
439
+ */
440
+ export function getRevealFeeForEstimation(config: TezosCoinConfig, address: string): number {
356
441
  const minFees = config.fees.minFees ?? 0
357
442
  return Math.max(minFees, getRevealFee(address))
358
443
  }
@@ -1,9 +1,8 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosConfig } from '../config'
4
+ import type { TezosContext } from '../config'
5
5
  import type { getTezosToolkit } from './tezosToolkit'
6
- import { log } from '@ledgerhq/logs'
7
6
  import { getRevealFee, getRevealGasLimit } from '@taquito/taquito'
8
7
 
9
8
  type RevealLimits = { fee: number; gasLimit: number; storageLimit: number }
@@ -14,19 +13,24 @@ type RevealLimits = { fee: number; gasLimit: number; storageLimit: number }
14
13
  * (some addresses error with "inconsistent_hash").
15
14
  */
16
15
  export async function estimateRevealLimits(
16
+ context: TezosContext,
17
17
  tezosToolkit: ReturnType<typeof getTezosToolkit>,
18
- address: string,
19
- feesConfig: TezosConfig['fees']
18
+ address: string
20
19
  ): Promise<RevealLimits> {
20
+ const { fees: feesConfig } = await context.config()
21
21
  type RevealEstimate = { gasLimit?: number; storageLimit?: number; suggestedFeeMutez?: number }
22
22
  let revealEstimate: RevealEstimate | undefined
23
23
  try {
24
24
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
25
25
  revealEstimate = (await tezosToolkit.estimate.reveal()) as RevealEstimate | undefined
26
26
  } catch (error) {
27
- log('estimate-error', 'error estimating reveal fees, trying to use getRevealGasLimit', {
28
- error,
29
- })
27
+ context.logger(
28
+ 'estimate-error',
29
+ 'error estimating reveal fees, trying to use getRevealGasLimit',
30
+ {
31
+ error,
32
+ }
33
+ )
30
34
  revealEstimate = {
31
35
  gasLimit: getRevealGasLimit(address),
32
36
  suggestedFeeMutez: getRevealFee(address),
@@ -3,7 +3,6 @@
3
3
 
4
4
  import type { TezosContext } from '../config'
5
5
  import type { Balance } from '@ledgerhq/coin-module-framework/api/index'
6
- import { log } from '@ledgerhq/logs'
7
6
  import { hasManagerKey } from '../network/types'
8
7
  import { createTzktApi } from '../network/tzkt'
9
8
  import { partitionNativeBalance } from '../utils'
@@ -60,14 +59,18 @@ export async function getBalance(context: TezosContext, address: string): Promis
60
59
  const normalized = BigInt(apiAccount.balance)
61
60
 
62
61
  const unstakeRequests = await fetchUnstakeRequests(config, address, apiAccount).catch((error) => {
63
- log('coin:tezos', 'getBalance: fetchUnstakeRequests failed; degrading stakes to []', {
64
- error,
65
- address,
66
- })
62
+ context.logger(
63
+ 'coin:tezos',
64
+ 'getBalance: fetchUnstakeRequests failed; degrading stakes to []',
65
+ {
66
+ error,
67
+ address,
68
+ }
69
+ )
67
70
  return []
68
71
  })
69
72
 
70
- const stakes = buildStakesForAccount(address, apiAccount, unstakeRequests)
73
+ const stakes = buildStakesForAccount(context.logger, address, apiAccount, unstakeRequests)
71
74
  const stakedBalance = BigInt(apiAccount.stakedBalance ?? 0)
72
75
  const unstakedBalance = BigInt(apiAccount.unstakedBalance ?? 0)
73
76
  const { locked } = partitionNativeBalance(normalized, stakedBalance, unstakedBalance)
@@ -534,12 +534,12 @@ export async function getBlock(context: TezosContext, height: number): Promise<B
534
534
  ] = await Promise.all([
535
535
  tzkt.getBlockByLevel(height),
536
536
  tzkt.getBlockByLevel(height - 1),
537
- fetchBlockTransactions(config, height),
538
- fetchBlockTokenTransfers(config, height),
539
- fetchBlockDelegations(config, height),
540
- fetchBlockStaking(config, height),
541
- fetchBlockOriginations(config, height),
542
- fetchBlockReveals(config, height),
537
+ fetchBlockTransactions(context, height),
538
+ fetchBlockTokenTransfers(context, height),
539
+ fetchBlockDelegations(context, height),
540
+ fetchBlockStaking(context, height),
541
+ fetchBlockOriginations(context, height),
542
+ fetchBlockReveals(context, height),
543
543
  ])
544
544
 
545
545
  // Token transfers triggered by originations from other blocks carry an
@@ -4,7 +4,7 @@
4
4
  import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import type { APIAccount, APIManagerAccount, APIUnstakeRequest } from '../network/types'
6
6
  import type { Cursor, Page, Stake } from '@ledgerhq/coin-module-framework/api/types'
7
- import { log } from '@ledgerhq/logs'
7
+ import type { Logger } from '@ledgerhq/coin-module-framework/config'
8
8
  import { hasManagerKey } from '../network/types'
9
9
  import { createTzktApi } from '../network/tzkt'
10
10
  import { partitionNativeBalance } from '../utils'
@@ -28,17 +28,21 @@ export function fetchUnstakeRequests(
28
28
  : Promise.resolve([])
29
29
  }
30
30
 
31
- function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake | null {
31
+ function unstakeRequestToStake(
32
+ logger: Logger,
33
+ address: string,
34
+ req: APIUnstakeRequest
35
+ ): Stake | null {
32
36
  // TzKT's status filter is unreliable; never build a position for a finalized request.
33
37
  if (req.status !== 'pending' && req.status !== 'finalizable') {
34
- log('coin:tezos', 'unstakeRequestToStake: dropping non-active unstake request', {
38
+ logger('coin:tezos', 'unstakeRequestToStake: dropping non-active unstake request', {
35
39
  requestId: req.id,
36
40
  status: req.status,
37
41
  })
38
42
  return null
39
43
  }
40
44
  if (req.actualAmount <= 0) {
41
- log('coin:tezos', 'unstakeRequestToStake: dropping non-positive unstake request', {
45
+ logger('coin:tezos', 'unstakeRequestToStake: dropping non-positive unstake request', {
42
46
  requestId: req.id,
43
47
  actualAmount: req.actualAmount,
44
48
  })
@@ -46,7 +50,7 @@ function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake |
46
50
  }
47
51
  const createdAt = new Date(req.firstTime)
48
52
  if (!Number.isFinite(createdAt.getTime())) {
49
- log('coin:tezos', 'unstakeRequestToStake: dropping unstake request with invalid firstTime', {
53
+ logger('coin:tezos', 'unstakeRequestToStake: dropping unstake request with invalid firstTime', {
50
54
  requestId: req.id,
51
55
  firstTime: req.firstTime,
52
56
  })
@@ -67,6 +71,7 @@ function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake |
67
71
  }
68
72
 
69
73
  export function buildStakesForAccount(
74
+ logger: Logger,
70
75
  address: string,
71
76
  account: APIManagerAccount,
72
77
  unstakeRequests: APIUnstakeRequest[]
@@ -80,7 +85,7 @@ export function buildStakesForAccount(
80
85
 
81
86
  const unstakeStakes: Stake[] = []
82
87
  for (const req of unstakeRequests) {
83
- const stake = unstakeRequestToStake(address, req)
88
+ const stake = unstakeRequestToStake(logger, address, req)
84
89
  if (stake) unstakeStakes.push(stake)
85
90
  }
86
91
 
@@ -93,7 +98,7 @@ export function buildStakesForAccount(
93
98
  unstakedBalance
94
99
  )
95
100
  if (locked < stakedBalance + unstakedBalance) {
96
- log('coin:tezos', 'buildStakesForAccount: balance < staked + unstaked, clamping to 0', {
101
+ logger('coin:tezos', 'buildStakesForAccount: balance < staked + unstaked, clamping to 0', {
97
102
  address,
98
103
  balance: balance.toString(),
99
104
  stakedBalance: stakedBalance.toString(),
@@ -137,5 +142,5 @@ export async function getStakes(
137
142
  const accountInfo = await createTzktApi(config).getAccountByAddress(address)
138
143
  if (!hasManagerKey(accountInfo)) return { items: [] }
139
144
  const unstakeRequests = await fetchUnstakeRequests(config, address, accountInfo)
140
- return { items: buildStakesForAccount(address, accountInfo, unstakeRequests) }
145
+ return { items: buildStakesForAccount(context.logger, address, accountInfo, unstakeRequests) }
141
146
  }