@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
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosCoinConfig, TezosContext } from '../config'
4
+ import type { TezosContext } from '../config'
5
5
  import type { AccountsGetOperationsOptions } from '../network/types'
6
6
  import type {
7
7
  APIDelegationType,
@@ -14,7 +14,7 @@ import type {
14
14
  TokenTransfersGetOptions,
15
15
  } from '../network/types'
16
16
  import type { Operation } from '@ledgerhq/coin-module-framework/api/types'
17
- import { log } from '@ledgerhq/logs'
17
+ import type { Logger } from '@ledgerhq/coin-module-framework/config'
18
18
  import { STAKING_ACTION_TO_OP_TYPE } from '../constants'
19
19
  import { createTzktApi } from '../network'
20
20
  import {
@@ -69,6 +69,7 @@ function maxLevel<T extends { level: number }>(items: T[]): number | undefined {
69
69
 
70
70
  /** When the API returned a full page, the trailing block level may be incomplete — drop it. */
71
71
  function trimPartialLastLevel<T extends { level: number; id: number }>(
72
+ logger: Logger,
72
73
  items: T[],
73
74
  full: boolean
74
75
  ): { trimmed: T[]; intraLevelLastId?: number } {
@@ -78,7 +79,7 @@ function trimPartialLastLevel<T extends { level: number; id: number }>(
78
79
  const lastLevel = last.level
79
80
  const filtered = items.filter((op) => op.level !== lastLevel)
80
81
  if (filtered.length === 0) {
81
- log('coin:tezos', 'listOperations: full page single level — keeping all rows', {
82
+ logger('coin:tezos', 'listOperations: full page single level — keeping all rows', {
82
83
  lastLevel,
83
84
  count: items.length,
84
85
  })
@@ -251,6 +252,7 @@ function keepNativeOp(op: APIOperation, address: string): op is ConvertibleOpera
251
252
  }
252
253
 
253
254
  function convertNativeOps(
255
+ logger: Logger,
254
256
  ops: APIOperation[],
255
257
  address: string,
256
258
  stakingBlockHashes: Map<number, string>
@@ -282,7 +284,7 @@ function convertNativeOps(
282
284
  return ops
283
285
  .filter((op): op is ConvertibleOperation => keepNativeOp(op, address))
284
286
  .map((op) => {
285
- const converted = convertOperation(address, op, stakingBlockHashes)
287
+ const converted = convertOperation(logger, address, op, stakingBlockHashes)
286
288
  const extraFee = internalFeesByHash.get(converted.tx.hash)
287
289
  if (extraFee) {
288
290
  // Add internal sub-tx fees to the parent operation, then clear to avoid
@@ -368,8 +370,8 @@ export async function listOperations(
368
370
  const nativePage = clampPage(nativeOpsRaw, limit)
369
371
  const tokenPage = clampPage(tokenTransfersRaw, limit)
370
372
 
371
- const nativeTrimmed = trimPartialLastLevel(nativePage.sliced, nativePage.full)
372
- const tokenTrimmed = trimPartialLastLevel(tokenPage.sliced, tokenPage.full)
373
+ const nativeTrimmed = trimPartialLastLevel(context.logger, nativePage.sliced, nativePage.full)
374
+ const tokenTrimmed = trimPartialLastLevel(context.logger, tokenPage.sliced, tokenPage.full)
373
375
 
374
376
  // when both streams are exhausted (neither hit the page limit),
375
377
  // we already have every op — alignment would silently discard ops
@@ -388,7 +390,7 @@ export async function listOperations(
388
390
  ? tokenTrimmed.trimmed
389
391
  : alignToBoundary(tokenTrimmed.trimmed, boundary, sort)
390
392
 
391
- const stakingBlockHashes = await fetchMissingStakingBlockHashes(config, nativeAligned)
393
+ const stakingBlockHashes = await fetchMissingStakingBlockHashes(context, nativeAligned)
392
394
 
393
395
  const parentMap = buildParentMap(nativeAligned)
394
396
 
@@ -401,7 +403,12 @@ export async function listOperations(
401
403
  tokenTrimmed.intraLevelLastId
402
404
  )
403
405
 
404
- const filteredNativeOps = convertNativeOps(nativeAligned, address, stakingBlockHashes)
406
+ const filteredNativeOps = convertNativeOps(
407
+ context.logger,
408
+ nativeAligned,
409
+ address,
410
+ stakingBlockHashes
411
+ )
405
412
  const tokenConverted = convertTokenOps(tokenAligned, parentMap, address)
406
413
 
407
414
  const sortedOperations = [...filteredNativeOps, ...tokenConverted].sort(
@@ -424,7 +431,7 @@ type ConvertibleOperation =
424
431
  * failures are swallowed: callers fall back to `""` for levels not in the map.
425
432
  */
426
433
  async function fetchMissingStakingBlockHashes(
427
- config: TezosCoinConfig,
434
+ context: TezosContext,
428
435
  ops: readonly APIOperation[]
429
436
  ): Promise<Map<number, string>> {
430
437
  const missingLevels = new Set<number>()
@@ -432,10 +439,11 @@ async function fetchMissingStakingBlockHashes(
432
439
  if (isAPIStakingType(op) && !op.block) missingLevels.add(op.level)
433
440
  }
434
441
  if (missingLevels.size === 0) return new Map()
442
+ const config = await context.config()
435
443
  try {
436
444
  return await createTzktApi(config).getBlockHashesByLevels([...missingLevels])
437
445
  } catch (err) {
438
- log('coin:tezos', 'fetchMissingStakingBlockHashes: skipped on fetch error', {
446
+ context.logger('coin:tezos', 'fetchMissingStakingBlockHashes: skipped on fetch error', {
439
447
  reason: String(err),
440
448
  })
441
449
  return new Map()
@@ -487,6 +495,7 @@ function resolveAmount(operation: ConvertibleOperation): bigint {
487
495
  }
488
496
 
489
497
  function resolveNormalizedType(
498
+ logger: Logger,
490
499
  operation: ConvertibleOperation,
491
500
  address: string,
492
501
  targetAddress: string | undefined,
@@ -499,7 +508,7 @@ function resolveNormalizedType(
499
508
  if (isAPIRevealType(operation)) return 'REVEAL'
500
509
  if (isAPIOriginationType(operation)) return operation.contractBalance > 0 ? 'OUT' : 'FEES'
501
510
  if (!isAPITransactionType(operation)) {
502
- log('coin:tezos', '(logic/operations): Unknown operation type, defaulting to OUT')
511
+ logger('coin:tezos', '(logic/operations): Unknown operation type, defaulting to OUT')
503
512
  return 'OUT'
504
513
  }
505
514
  const isOut = operation.sender?.address === address
@@ -529,6 +538,7 @@ function getLedgerOpType(
529
538
  }
530
539
 
531
540
  function convertOperation(
541
+ logger: Logger,
532
542
  address: string,
533
543
  operation: ConvertibleOperation,
534
544
  stakingBlockHashes: Map<number, string>
@@ -544,7 +554,7 @@ function convertOperation(
544
554
  const targetAddress = resolveTargetAddress(operation)
545
555
  // reveal has no meaningful target; staking resolves senders/recipients separately.
546
556
  if (!targetAddress && !isAPIRevealType(operation) && !isAPIStakingType(operation)) {
547
- log('coin:tezos', '(logic/operations): No target address found for operation', operation)
557
+ logger('coin:tezos', '(logic/operations): No target address found for operation', operation)
548
558
  }
549
559
 
550
560
  const { senders, recipients } = isAPIStakingType(operation)
@@ -561,7 +571,7 @@ function convertOperation(
561
571
  BigInt(operation.bakerFee ?? 0) +
562
572
  BigInt(operation.allocationFee ?? 0)
563
573
 
564
- const normalizedType = resolveNormalizedType(operation, address, targetAddress, amount)
574
+ const normalizedType = resolveNormalizedType(logger, operation, address, targetAddress, amount)
565
575
 
566
576
  // Tezos uses "applied" for every success operation (something else=failed )
567
577
  const hasFailed = operation.status && operation.status !== 'applied'
@@ -15,9 +15,10 @@ import {
15
15
  MustDelegateBeforeStaking,
16
16
  NotEnoughBalanceToDelegate,
17
17
  TezosNotEnoughStaked,
18
+ TezosStakeAmountTooSmall,
18
19
  TezosStakeBlockedByPendingUnstake,
19
20
  } from '../types/errors'
20
- import { STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
21
+ import { MAPPED_TAQUITO_ERRORS, STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
21
22
  import { validateIntent } from './validateIntent'
22
23
 
23
24
  const mockEstimateFees = jest.fn()
@@ -387,6 +388,35 @@ describe('validateIntent', () => {
387
388
  expect(result.amount).toBe(0n)
388
389
  })
389
390
 
391
+ it('should return TezosStakeAmountTooSmall when stake useAllAmount resolves max below the probe', async () => {
392
+ mockGetAccountByAddress.mockResolvedValue(
393
+ makeUserAccount({
394
+ delegate: { alias: 'baker', address: validRecipient, active: true },
395
+ delegationLevel: 1,
396
+ })
397
+ )
398
+ mockEstimateFees.mockResolvedValueOnce({
399
+ fees: 1000n,
400
+ gasLimit: 10000n,
401
+ storageLimit: 0n,
402
+ estimatedFees: 1000n,
403
+ amount: 999n,
404
+ })
405
+
406
+ const result = await validateIntent(context, {
407
+ intentType: 'staking',
408
+ asset: { type: 'native' },
409
+ type: 'stake',
410
+ sender: senderAddress,
411
+ recipient: '',
412
+ amount: 0n,
413
+ useAllAmount: true,
414
+ })
415
+
416
+ expect(result.errors.amount).toBeInstanceOf(TezosStakeAmountTooSmall)
417
+ expect(result.amount).toBe(999n)
418
+ })
419
+
390
420
  it('excludes already-staked funds when stake useAllAmount falls back to balance computation', async () => {
391
421
  mockGetAccountByAddress.mockResolvedValue(
392
422
  makeUserAccount({
@@ -987,6 +1017,65 @@ describe('validateIntent', () => {
987
1017
  expect(result.errors.amount).toBeInstanceOf(MustDelegateBeforeStaking)
988
1018
  })
989
1019
 
1020
+ it('maps stake_amount_too_small to TezosStakeAmountTooSmall', async () => {
1021
+ mockGetAccountByAddress.mockResolvedValue(
1022
+ makeUserAccount({
1023
+ delegate: { alias: 'baker', address: validRecipient, active: true },
1024
+ delegationLevel: 1,
1025
+ })
1026
+ )
1027
+ mockEstimateFees.mockResolvedValue({
1028
+ fees: 0n,
1029
+ gasLimit: 0n,
1030
+ storageLimit: 0n,
1031
+ estimatedFees: 500n,
1032
+ taquitoError: 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small',
1033
+ })
1034
+
1035
+ const result = await validateIntent(context, {
1036
+ intentType: 'staking',
1037
+ asset: { type: 'native' },
1038
+ type: 'stake',
1039
+ sender: senderAddress,
1040
+ recipient: '',
1041
+ amount: 1n,
1042
+ })
1043
+
1044
+ expect(result.errors.amount).toBeInstanceOf(TezosStakeAmountTooSmall)
1045
+ })
1046
+
1047
+ it.each(MAPPED_TAQUITO_ERRORS)(
1048
+ 'maps %s, which estimation reports instead of throwing, to a dedicated error',
1049
+ async (id) => {
1050
+ mockGetAccountByAddress.mockResolvedValue(
1051
+ makeUserAccount({
1052
+ delegate: { alias: 'baker', address: validRecipient, active: true },
1053
+ delegationLevel: 1,
1054
+ })
1055
+ )
1056
+ mockEstimateFees.mockResolvedValue({
1057
+ fees: 0n,
1058
+ gasLimit: 0n,
1059
+ storageLimit: 0n,
1060
+ estimatedFees: 500n,
1061
+ taquitoError: `proto.025-PsUshuai.${id}`,
1062
+ })
1063
+
1064
+ const result = await validateIntent(context, {
1065
+ intentType: 'staking',
1066
+ asset: { type: 'native' },
1067
+ type: 'stake',
1068
+ sender: senderAddress,
1069
+ recipient: '',
1070
+ amount: 1000n,
1071
+ })
1072
+
1073
+ const error = result.errors.amount ?? result.errors.recipient
1074
+ expect(error).toBeInstanceOf(Error)
1075
+ expect(error?.name).not.toBe('Error')
1076
+ }
1077
+ )
1078
+
990
1079
  it('maps cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate to TezosStakeBlockedByPendingUnstake for stake', async () => {
991
1080
  mockGetAccountByAddress.mockResolvedValue(
992
1081
  makeUserAccount({
@@ -24,6 +24,7 @@ import {
24
24
  MustDelegateBeforeStaking,
25
25
  NotEnoughBalanceToDelegate,
26
26
  TezosNotEnoughStaked,
27
+ TezosStakeAmountTooSmall,
27
28
  TezosStakeBlockedByPendingUnstake,
28
29
  } from '../types/errors'
29
30
  import {
@@ -31,6 +32,7 @@ import {
31
32
  parseTezosTokenAsset,
32
33
  partitionNativeBalance,
33
34
  resolveTezosOperationMode,
35
+ STAKE_ESTIMATION_PROBE_MUTEZ,
34
36
  } from '../utils'
35
37
  import { estimateFees } from './estimateFees'
36
38
 
@@ -156,6 +158,9 @@ function mapTaquitoErrors(taquitoError: string, intentType: string): Record<stri
156
158
  errors.amount = new TezosNotEnoughStaked()
157
159
  } else if (taquitoError.endsWith('contract.must_be_delegated_to_stake')) {
158
160
  errors.amount = new MustDelegateBeforeStaking()
161
+ } else if (taquitoError.endsWith('stake_amount_too_small')) {
162
+ // Below one staking pseudotoken the stake mints nothing; see STAKE_ESTIMATION_PROBE_MUTEZ.
163
+ errors.amount = new TezosStakeAmountTooSmall()
159
164
  } else if (
160
165
  taquitoError.endsWith('cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate')
161
166
  ) {
@@ -166,10 +171,10 @@ function mapTaquitoErrors(taquitoError: string, intentType: string): Record<stri
166
171
  // Re-delegating (or staking) to the current baker leaves the delegate unchanged; the node
167
172
  // rejects it. Surfaces for both `delegate` and `stake` intents as "already delegated".
168
173
  errors.recipient = new InvalidAddressBecauseAlreadyDelegated()
169
- } else if (taquitoError.includes('empty_implicit_contract')) {
174
+ } else if (taquitoError.endsWith('empty_implicit_contract')) {
170
175
  errors.amount =
171
176
  intentType === 'stake' ? new NotEnoughBalance() : new NotEnoughBalanceToDelegate()
172
- } else if (taquitoError.includes('script_rejected')) {
177
+ } else if (taquitoError.endsWith('script_rejected')) {
173
178
  errors.amount = new NotEnoughBalance()
174
179
  } else {
175
180
  errors.amount = new Error(taquitoError)
@@ -447,6 +452,17 @@ export async function validateIntent(
447
452
  errors.amount = new NotEnoughBalance()
448
453
  }
449
454
 
455
+ // Estimation simulates the probe, never the resolved max; below the probe the stake may still
456
+ // mint no pseudotoken and be refused at injection.
457
+ if (
458
+ intent.type === 'stake' &&
459
+ intent.useAllAmount &&
460
+ amount < STAKE_ESTIMATION_PROBE_MUTEZ &&
461
+ !errors.amount
462
+ ) {
463
+ errors.amount = new TezosStakeAmountTooSmall()
464
+ }
465
+
450
466
  const { spendable } = partitionNativeBalance(
451
467
  BigInt(senderInfo.balance),
452
468
  BigInt(senderInfo.stakedBalance ?? 0),
@@ -1,16 +1,17 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosCoinConfig } from '../config'
4
+ import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import { listBakers } from '../network/bakers'
6
6
  import whitelist from '../network/bakers.whitelist-default'
7
7
  import { mockConfig } from '../test/config'
8
8
 
9
9
  describe('tezos bakers', () => {
10
10
  const config: TezosCoinConfig = mockConfig
11
+ const context: TezosContext = { config: async () => config, logger: jest.fn() }
11
12
 
12
13
  test('atleast 10 whitelisted bakers are online', async () => {
13
- const onlineBakersCount = (await listBakers(config, whitelist))
14
+ const onlineBakersCount = (await listBakers(context, whitelist))
14
15
  .map((baker) => baker.address)
15
16
  .filter((address) => whitelist.includes(address)).length
16
17
  expect(onlineBakersCount).toBeGreaterThan(10)
@@ -7,6 +7,7 @@ import { mockConfig } from '../test/config'
7
7
  import { asBaker, cache } from './bakers'
8
8
 
9
9
  const mockNetworkFn = jest.fn()
10
+ const mockLogger = jest.fn()
10
11
  jest.mock('@ledgerhq/live-network/network', () => ({
11
12
  __esModule: true,
12
13
  default: (...args: unknown[]) => mockNetworkFn(...args),
@@ -183,7 +184,7 @@ describe('Tezos Baker', () => {
183
184
  })
184
185
 
185
186
  it('Ledger Baker should be first in the list', async () => {
186
- const bakers = await cache(config)
187
+ const bakers = await cache(config, mockLogger)
187
188
  expect(bakers[0].name).toBe('Ledger by Kiln')
188
189
  })
189
190
  })
@@ -1,11 +1,11 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosCoinConfig } from '../config'
4
+ import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import type { Baker } from '../types'
6
+ import type { Logger } from '@ledgerhq/coin-module-framework/config'
6
7
  import { makeLRUCache } from '@ledgerhq/live-network/cache'
7
8
  import network from '@ledgerhq/live-network/network'
8
- import { log } from '@ledgerhq/logs'
9
9
  import { ledgerValidatorAddress } from './bakers.whitelist-default'
10
10
 
11
11
  export type TezosApiBaker = {
@@ -40,7 +40,7 @@ export type TezosBakerFeature = {
40
40
  }
41
41
 
42
42
  export const cache = makeLRUCache(
43
- async (config: TezosCoinConfig): Promise<Baker[]> => {
43
+ async (config: TezosCoinConfig, logger: Logger): Promise<Baker[]> => {
44
44
  const bakers: Baker[] = []
45
45
  const TEZOS_API_BASE_URL = config.baker.url
46
46
  const { data } = await network<TezosApiBaker[]>({
@@ -48,7 +48,7 @@ export const cache = makeLRUCache(
48
48
  })
49
49
 
50
50
  if (data && Array.isArray(data)) {
51
- log('tezos/bakers', 'found ' + data.length + ' bakers')
51
+ logger('tezos/bakers', 'found ' + data.length + ' bakers')
52
52
  data
53
53
  .filter((raw) => raw.status === 'active' && raw.delegation.enabled)
54
54
  .forEach((raw) => {
@@ -64,7 +64,7 @@ export const cache = makeLRUCache(
64
64
  const ledgerBakerIndex = bakers.findIndex((baker) => baker.address === ledgerValidatorAddress)
65
65
  const [ledgerBaker] = bakers.splice(ledgerBakerIndex, 1)
66
66
 
67
- log('tezos/bakers', 'loaded ' + bakers.length + ' bakers')
67
+ logger('tezos/bakers', 'loaded ' + bakers.length + ' bakers')
68
68
  return [ledgerBaker, ...bakers]
69
69
  },
70
70
  (config: TezosCoinConfig) => config.baker.url
@@ -72,8 +72,9 @@ export const cache = makeLRUCache(
72
72
 
73
73
  let _lastBakers: Baker[] = []
74
74
 
75
- export const fetchAllBakers = async (config: TezosCoinConfig): Promise<Baker[]> => {
76
- const allBakers = await cache.force(config)
75
+ export const fetchAllBakers = async (context: TezosContext): Promise<Baker[]> => {
76
+ const config = await context.config()
77
+ const allBakers = await cache.force(config, context.logger)
77
78
  _lastBakers = allBakers
78
79
  return allBakers
79
80
  }
@@ -86,10 +87,11 @@ export const listBakersWithDefault = (whitelistAddresses: string[]): Baker[] =>
86
87
  }
87
88
 
88
89
  export const listBakers = async (
89
- config: TezosCoinConfig,
90
+ context: TezosContext,
90
91
  whitelistAddresses: string[]
91
92
  ): Promise<Baker[]> => {
92
- _lastBakers = await cache(config)
93
+ const config = await context.config()
94
+ _lastBakers = await cache(config, context.logger)
93
95
  return whitelist(_lastBakers, whitelistAddresses)
94
96
  }
95
97
 
@@ -97,12 +99,12 @@ export function getBakerSync(addr: string): Baker | undefined {
97
99
  return _lastBakers.find((baker) => baker.address === addr)
98
100
  }
99
101
 
100
- export async function loadBaker(config: TezosCoinConfig, addr: string): Promise<Baker | undefined> {
102
+ export async function loadBaker(context: TezosContext, addr: string): Promise<Baker | undefined> {
101
103
  const cacheBaker = getBakerSync(addr)
102
104
  if (cacheBaker) return cacheBaker
103
- const bakers = await cache(config)
104
- const baker = bakers.find((baker) => baker.address === addr)
105
- return baker
105
+ const config = await context.config()
106
+ const bakers = await cache(config, context.logger)
107
+ return bakers.find((baker) => baker.address === addr)
106
108
  }
107
109
 
108
110
  export const asBaker = (data: TezosApiBaker): Baker | undefined => {
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosCoinConfig } from '../config'
4
+ import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  // Import fetchBlockTransactions / fetchBlockTokenTransfers / fetchBlockDelegations through the index re-export
6
6
  // so that network/index.ts line 4 is exercised and its coverage is tracked.
7
7
  import type {
@@ -18,7 +18,6 @@ import type { DeepPartial, DeepPartialReturn } from '@ledgerhq/coin-module-frame
18
18
  // tzkt.ts imports `network` from "@ledgerhq/live-network" (the package root, not the /network
19
19
  // subpath). We mock the same path so Jest's module registry intercepts those calls.
20
20
  import network from '@ledgerhq/live-network'
21
- import { log } from '@ledgerhq/logs'
22
21
  import { mockConfig } from '../test/config'
23
22
  import { createTzktApi } from './tzkt'
24
23
  import {
@@ -30,9 +29,10 @@ import {
30
29
  } from '.'
31
30
 
32
31
  jest.mock('@ledgerhq/live-network')
33
- jest.mock('@ledgerhq/logs')
34
32
  const mockedNetwork = jest.mocked<DeepPartialReturn<typeof network>>(network)
35
- const mockedLog = jest.mocked(log)
33
+ const mockLogger = jest.fn()
34
+ const config: TezosCoinConfig = mockConfig
35
+ const context: TezosContext = { config: async () => config, logger: mockLogger }
36
36
 
37
37
  // The tzkt module is NOT mocked here. `createTzktApi(config)` returns a plain object whose methods
38
38
  // close over `config.explorer.url` and delegate to the mocked `network`. Each method also calls its
@@ -43,7 +43,6 @@ const mockedLog = jest.mocked(log)
43
43
  // The paginated helpers (`fetchAllTransactions`, `fetchBlock*`) are top-level functions that build
44
44
  // their OWN fresh `createTzktApi(config)` instance internally, so spying on `api` cannot intercept
45
45
  // them. Those tests therefore drive the mocked `network` directly.
46
- const config: TezosCoinConfig = mockConfig
47
46
  const api = createTzktApi(config)
48
47
 
49
48
  // ---------------------------------------------------------------------------
@@ -808,7 +807,7 @@ describe('tzkt network API', () => {
808
807
  it('returns empty when the first page is empty', async () => {
809
808
  mockedNetwork.mockReturnValue(networkResponse([]))
810
809
 
811
- const result = await fetchAllTransactions(config, 'tz1acc')
810
+ const result = await fetchAllTransactions(context, 'tz1acc')
812
811
 
813
812
  expect(result).toEqual([])
814
813
  })
@@ -819,7 +818,7 @@ describe('tzkt network API', () => {
819
818
  .mockReturnValueOnce(networkResponse([makeOperation(2)]))
820
819
  .mockReturnValueOnce(networkResponse([]))
821
820
 
822
- const result = await fetchAllTransactions(config, 'tz1acc')
821
+ const result = await fetchAllTransactions(context, 'tz1acc')
823
822
 
824
823
  expect(result).toEqual([makeOperation(1), makeOperation(2)])
825
824
  expect(mockedNetwork).toHaveBeenCalledTimes(3)
@@ -840,10 +839,10 @@ describe('tzkt network API', () => {
840
839
  const opMissingId: DeepPartial<APIOperation> = { ...makeOperation(1), id: 0 }
841
840
  mockedNetwork.mockReturnValue(networkResponse([opMissingId]))
842
841
 
843
- const result = await fetchAllTransactions(config, 'tz1acc')
842
+ const result = await fetchAllTransactions(context, 'tz1acc')
844
843
 
845
844
  expect(result).toEqual([opMissingId])
846
- expect(mockedLog).toHaveBeenCalledWith('tezos', 'id missing!')
845
+ expect(mockLogger).toHaveBeenCalledWith('tezos', 'id missing!')
847
846
  })
848
847
 
849
848
  it('stops after maxTxQuery iterations', async () => {
@@ -852,7 +851,7 @@ describe('tzkt network API', () => {
852
851
  let nextId = 0
853
852
  mockedNetwork.mockImplementation(() => networkResponse([makeOperation(++nextId)]))
854
853
 
855
- const result = await fetchAllTransactions(config, 'tz1acc')
854
+ const result = await fetchAllTransactions(context, 'tz1acc')
856
855
 
857
856
  expect(mockedNetwork).toHaveBeenCalledTimes(mockConfig.explorer.maxTxQuery)
858
857
  expect(result).toHaveLength(mockConfig.explorer.maxTxQuery)
@@ -992,7 +991,7 @@ describe('tzkt network API', () => {
992
991
  it('returns an empty array when the first page is empty', async () => {
993
992
  mockedNetwork.mockReturnValue(networkResponse([]))
994
993
 
995
- const result = await fetchBlockTransactions(config, 100)
994
+ const result = await fetchBlockTransactions(context, 100)
996
995
 
997
996
  expect(result).toEqual([])
998
997
  })
@@ -1001,7 +1000,7 @@ describe('tzkt network API', () => {
1001
1000
  const txs = [makeTxItem(1), makeTxItem(2)]
1002
1001
  mockedNetwork.mockReturnValue(networkResponse(txs))
1003
1002
 
1004
- const result = await fetchBlockTransactions(config, 100)
1003
+ const result = await fetchBlockTransactions(context, 100)
1005
1004
 
1006
1005
  expect(result).toEqual(txs)
1007
1006
  })
@@ -1014,7 +1013,7 @@ describe('tzkt network API', () => {
1014
1013
  .mockReturnValueOnce(networkResponse(fullPage))
1015
1014
  .mockReturnValueOnce(networkResponse(lastPage))
1016
1015
 
1017
- const result = await fetchBlockTransactions(config, 100)
1016
+ const result = await fetchBlockTransactions(context, 100)
1018
1017
 
1019
1018
  expect(result).toHaveLength(1002)
1020
1019
  expect(mockedNetwork).toHaveBeenCalledTimes(2)
@@ -1031,11 +1030,11 @@ describe('tzkt network API', () => {
1031
1030
  const fullPage = Array.from({ length: 1000 }, (_, i) => makeTxItem(i + 1))
1032
1031
  mockedNetwork.mockReturnValue(networkResponse(fullPage))
1033
1032
 
1034
- const result = await fetchBlockTransactions(config, 100)
1033
+ const result = await fetchBlockTransactions(context, 100)
1035
1034
 
1036
1035
  expect(mockedNetwork).toHaveBeenCalledTimes(100)
1037
1036
  expect(result).toHaveLength(100_000)
1038
- expect(mockedLog).toHaveBeenCalledWith(
1037
+ expect(mockLogger).toHaveBeenCalledWith(
1039
1038
  'tezos',
1040
1039
  expect.stringContaining('fetchBlockTransactions: maxTxQuery limit reached at level 100')
1041
1040
  )
@@ -1051,7 +1050,7 @@ describe('tzkt network API', () => {
1051
1050
  it('returns an empty array when the first page is empty', async () => {
1052
1051
  mockedNetwork.mockReturnValue(networkResponse([]))
1053
1052
 
1054
- const result = await fetchBlockTokenTransfers(config, 100)
1053
+ const result = await fetchBlockTokenTransfers(context, 100)
1055
1054
 
1056
1055
  expect(result).toEqual([])
1057
1056
  })
@@ -1060,7 +1059,7 @@ describe('tzkt network API', () => {
1060
1059
  const items = [makeTokenItem(1), makeTokenItem(2)]
1061
1060
  mockedNetwork.mockReturnValue(networkResponse(items))
1062
1061
 
1063
- const result = await fetchBlockTokenTransfers(config, 100)
1062
+ const result = await fetchBlockTokenTransfers(context, 100)
1064
1063
 
1065
1064
  expect(result).toEqual(items)
1066
1065
  })
@@ -1073,7 +1072,7 @@ describe('tzkt network API', () => {
1073
1072
  .mockReturnValueOnce(networkResponse(fullPage))
1074
1073
  .mockReturnValueOnce(networkResponse(lastPage))
1075
1074
 
1076
- const result = await fetchBlockTokenTransfers(config, 100)
1075
+ const result = await fetchBlockTokenTransfers(context, 100)
1077
1076
 
1078
1077
  expect(result).toHaveLength(1001)
1079
1078
  expect(mockedNetwork).toHaveBeenCalledTimes(2)
@@ -1088,11 +1087,11 @@ describe('tzkt network API', () => {
1088
1087
  const fullPage = Array.from({ length: 1000 }, (_, i) => makeTokenItem(i + 1))
1089
1088
  mockedNetwork.mockReturnValue(networkResponse(fullPage))
1090
1089
 
1091
- const result = await fetchBlockTokenTransfers(config, 100)
1090
+ const result = await fetchBlockTokenTransfers(context, 100)
1092
1091
 
1093
1092
  expect(mockedNetwork).toHaveBeenCalledTimes(100)
1094
1093
  expect(result).toHaveLength(100_000)
1095
- expect(mockedLog).toHaveBeenCalledWith(
1094
+ expect(mockLogger).toHaveBeenCalledWith(
1096
1095
  'tezos',
1097
1096
  expect.stringContaining('fetchBlockTokenTransfers: maxTxQuery limit reached at level 100')
1098
1097
  )
@@ -1147,7 +1146,7 @@ describe('tzkt network API', () => {
1147
1146
  it('returns an empty array when the first page is empty', async () => {
1148
1147
  mockedNetwork.mockReturnValue(networkResponse([]))
1149
1148
 
1150
- const result = await fetchBlockDelegations(config, 100)
1149
+ const result = await fetchBlockDelegations(context, 100)
1151
1150
 
1152
1151
  expect(result).toEqual([])
1153
1152
  })
@@ -1156,7 +1155,7 @@ describe('tzkt network API', () => {
1156
1155
  const items = [makeDelegationItem(1), makeDelegationItem(2)]
1157
1156
  mockedNetwork.mockReturnValue(networkResponse(items))
1158
1157
 
1159
- const result = await fetchBlockDelegations(config, 100)
1158
+ const result = await fetchBlockDelegations(context, 100)
1160
1159
 
1161
1160
  expect(result).toEqual(items)
1162
1161
  })
@@ -1169,7 +1168,7 @@ describe('tzkt network API', () => {
1169
1168
  .mockReturnValueOnce(networkResponse(fullPage))
1170
1169
  .mockReturnValueOnce(networkResponse(lastPage))
1171
1170
 
1172
- const result = await fetchBlockDelegations(config, 100)
1171
+ const result = await fetchBlockDelegations(context, 100)
1173
1172
 
1174
1173
  expect(result).toHaveLength(1001)
1175
1174
  expect(mockedNetwork).toHaveBeenCalledTimes(2)
@@ -1184,11 +1183,11 @@ describe('tzkt network API', () => {
1184
1183
  const fullPage = Array.from({ length: 1000 }, (_, i) => makeDelegationItem(i + 1))
1185
1184
  mockedNetwork.mockReturnValue(networkResponse(fullPage))
1186
1185
 
1187
- const result = await fetchBlockDelegations(config, 100)
1186
+ const result = await fetchBlockDelegations(context, 100)
1188
1187
 
1189
1188
  expect(mockedNetwork).toHaveBeenCalledTimes(100)
1190
1189
  expect(result).toHaveLength(100_000)
1191
- expect(mockedLog).toHaveBeenCalledWith(
1190
+ expect(mockLogger).toHaveBeenCalledWith(
1192
1191
  'tezos',
1193
1192
  expect.stringContaining('fetchBlockDelegations: maxTxQuery limit reached at level 100')
1194
1193
  )
@@ -1243,7 +1242,7 @@ describe('tzkt network API', () => {
1243
1242
  it('returns an empty array when the first page is empty', async () => {
1244
1243
  mockedNetwork.mockReturnValue(networkResponse([]))
1245
1244
 
1246
- const result = await fetchBlockStaking(config, 100)
1245
+ const result = await fetchBlockStaking(context, 100)
1247
1246
 
1248
1247
  expect(result).toEqual([])
1249
1248
  })
@@ -1252,7 +1251,7 @@ describe('tzkt network API', () => {
1252
1251
  const items = [makeStakingItem(1), makeStakingItem(2)]
1253
1252
  mockedNetwork.mockReturnValue(networkResponse(items))
1254
1253
 
1255
- const result = await fetchBlockStaking(config, 100)
1254
+ const result = await fetchBlockStaking(context, 100)
1256
1255
 
1257
1256
  expect(result).toEqual(items)
1258
1257
  })
@@ -1265,7 +1264,7 @@ describe('tzkt network API', () => {
1265
1264
  .mockReturnValueOnce(networkResponse(fullPage))
1266
1265
  .mockReturnValueOnce(networkResponse(lastPage))
1267
1266
 
1268
- const result = await fetchBlockStaking(config, 100)
1267
+ const result = await fetchBlockStaking(context, 100)
1269
1268
 
1270
1269
  expect(result).toHaveLength(1001)
1271
1270
  expect(mockedNetwork).toHaveBeenCalledTimes(2)
@@ -1280,11 +1279,11 @@ describe('tzkt network API', () => {
1280
1279
  const fullPage = Array.from({ length: 1000 }, (_, i) => makeStakingItem(i + 1))
1281
1280
  mockedNetwork.mockReturnValue(networkResponse(fullPage))
1282
1281
 
1283
- const result = await fetchBlockStaking(config, 100)
1282
+ const result = await fetchBlockStaking(context, 100)
1284
1283
 
1285
1284
  expect(mockedNetwork).toHaveBeenCalledTimes(100)
1286
1285
  expect(result).toHaveLength(100_000)
1287
- expect(mockedLog).toHaveBeenCalledWith(
1286
+ expect(mockLogger).toHaveBeenCalledWith(
1288
1287
  'tezos',
1289
1288
  expect.stringContaining('fetchBlockStaking: maxTxQuery limit reached at level 100')
1290
1289
  )