@ledgerhq/coin-tezos 7.12.0 → 9.0.0

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 (199) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/lib/api/index.d.ts +1 -2
  3. package/lib/api/index.d.ts.map +1 -1
  4. package/lib/api/index.js +45 -44
  5. package/lib/api/index.js.map +1 -1
  6. package/lib/api/types.d.ts +3 -2
  7. package/lib/api/types.d.ts.map +1 -1
  8. package/lib/config.d.ts +3 -1
  9. package/lib/config.d.ts.map +1 -1
  10. package/lib/config.js.map +1 -1
  11. package/lib/logic/broadcast.d.ts +2 -1
  12. package/lib/logic/broadcast.d.ts.map +1 -1
  13. package/lib/logic/broadcast.js +3 -2
  14. package/lib/logic/broadcast.js.map +1 -1
  15. package/lib/logic/combine.d.ts +1 -1
  16. package/lib/logic/combine.d.ts.map +1 -1
  17. package/lib/logic/combine.js +4 -1
  18. package/lib/logic/combine.js.map +1 -1
  19. package/lib/logic/craftRawOperations.d.ts +2 -1
  20. package/lib/logic/craftRawOperations.d.ts.map +1 -1
  21. package/lib/logic/craftRawOperations.js +5 -8
  22. package/lib/logic/craftRawOperations.js.map +1 -1
  23. package/lib/logic/craftTransaction.d.ts +3 -2
  24. package/lib/logic/craftTransaction.d.ts.map +1 -1
  25. package/lib/logic/craftTransaction.js +6 -9
  26. package/lib/logic/craftTransaction.js.map +1 -1
  27. package/lib/logic/estimateFees.d.ts +2 -1
  28. package/lib/logic/estimateFees.d.ts.map +1 -1
  29. package/lib/logic/estimateFees.js +24 -19
  30. package/lib/logic/estimateFees.js.map +1 -1
  31. package/lib/logic/getBalance.d.ts +2 -1
  32. package/lib/logic/getBalance.d.ts.map +1 -1
  33. package/lib/logic/getBalance.js +7 -8
  34. package/lib/logic/getBalance.js.map +1 -1
  35. package/lib/logic/getBlock.d.ts +2 -1
  36. package/lib/logic/getBlock.d.ts.map +1 -1
  37. package/lib/logic/getBlock.js +12 -10
  38. package/lib/logic/getBlock.js.map +1 -1
  39. package/lib/logic/getBlockInfo.d.ts +2 -1
  40. package/lib/logic/getBlockInfo.d.ts.map +1 -1
  41. package/lib/logic/getBlockInfo.js +5 -3
  42. package/lib/logic/getBlockInfo.js.map +1 -1
  43. package/lib/logic/getStakes.d.ts +3 -2
  44. package/lib/logic/getStakes.d.ts.map +1 -1
  45. package/lib/logic/getStakes.js +9 -9
  46. package/lib/logic/getStakes.js.map +1 -1
  47. package/lib/logic/lastBlock.d.ts +2 -1
  48. package/lib/logic/lastBlock.d.ts.map +1 -1
  49. package/lib/logic/lastBlock.js +4 -6
  50. package/lib/logic/lastBlock.js.map +1 -1
  51. package/lib/logic/listOperations.d.ts +2 -1
  52. package/lib/logic/listOperations.d.ts.map +1 -1
  53. package/lib/logic/listOperations.js +8 -6
  54. package/lib/logic/listOperations.js.map +1 -1
  55. package/lib/logic/tezosToolkit.d.ts +2 -1
  56. package/lib/logic/tezosToolkit.d.ts.map +1 -1
  57. package/lib/logic/tezosToolkit.js +2 -6
  58. package/lib/logic/tezosToolkit.js.map +1 -1
  59. package/lib/logic/validateIntent.d.ts +2 -1
  60. package/lib/logic/validateIntent.d.ts.map +1 -1
  61. package/lib/logic/validateIntent.js +12 -13
  62. package/lib/logic/validateIntent.js.map +1 -1
  63. package/lib/network/bakers.d.ts +5 -4
  64. package/lib/network/bakers.d.ts.map +1 -1
  65. package/lib/network/bakers.js +9 -10
  66. package/lib/network/bakers.js.map +1 -1
  67. package/lib/network/index.d.ts +1 -1
  68. package/lib/network/index.d.ts.map +1 -1
  69. package/lib/network/index.js +2 -5
  70. package/lib/network/index.js.map +1 -1
  71. package/lib/network/tzkt.d.ts +16 -9
  72. package/lib/network/tzkt.d.ts.map +1 -1
  73. package/lib/network/tzkt.js +341 -333
  74. package/lib/network/tzkt.js.map +1 -1
  75. package/lib/utils.d.ts +2 -1
  76. package/lib/utils.d.ts.map +1 -1
  77. package/lib/utils.js +1 -6
  78. package/lib/utils.js.map +1 -1
  79. package/lib-es/api/index.d.ts +1 -2
  80. package/lib-es/api/index.d.ts.map +1 -1
  81. package/lib-es/api/index.js +40 -36
  82. package/lib-es/api/index.js.map +1 -1
  83. package/lib-es/api/types.d.ts +3 -2
  84. package/lib-es/api/types.d.ts.map +1 -1
  85. package/lib-es/config.d.ts +3 -1
  86. package/lib-es/config.d.ts.map +1 -1
  87. package/lib-es/config.js.map +1 -1
  88. package/lib-es/logic/broadcast.d.ts +2 -1
  89. package/lib-es/logic/broadcast.d.ts.map +1 -1
  90. package/lib-es/logic/broadcast.js +3 -2
  91. package/lib-es/logic/broadcast.js.map +1 -1
  92. package/lib-es/logic/combine.d.ts +1 -1
  93. package/lib-es/logic/combine.d.ts.map +1 -1
  94. package/lib-es/logic/combine.js +4 -1
  95. package/lib-es/logic/combine.js.map +1 -1
  96. package/lib-es/logic/craftRawOperations.d.ts +2 -1
  97. package/lib-es/logic/craftRawOperations.d.ts.map +1 -1
  98. package/lib-es/logic/craftRawOperations.js +5 -5
  99. package/lib-es/logic/craftRawOperations.js.map +1 -1
  100. package/lib-es/logic/craftTransaction.d.ts +3 -2
  101. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  102. package/lib-es/logic/craftTransaction.js +6 -6
  103. package/lib-es/logic/craftTransaction.js.map +1 -1
  104. package/lib-es/logic/estimateFees.d.ts +2 -1
  105. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  106. package/lib-es/logic/estimateFees.js +24 -16
  107. package/lib-es/logic/estimateFees.js.map +1 -1
  108. package/lib-es/logic/getBalance.d.ts +2 -1
  109. package/lib-es/logic/getBalance.d.ts.map +1 -1
  110. package/lib-es/logic/getBalance.js +5 -3
  111. package/lib-es/logic/getBalance.js.map +1 -1
  112. package/lib-es/logic/getBlock.d.ts +2 -1
  113. package/lib-es/logic/getBlock.d.ts.map +1 -1
  114. package/lib-es/logic/getBlock.js +10 -8
  115. package/lib-es/logic/getBlock.js.map +1 -1
  116. package/lib-es/logic/getBlockInfo.d.ts +2 -1
  117. package/lib-es/logic/getBlockInfo.d.ts.map +1 -1
  118. package/lib-es/logic/getBlockInfo.js +4 -2
  119. package/lib-es/logic/getBlockInfo.js.map +1 -1
  120. package/lib-es/logic/getStakes.d.ts +3 -2
  121. package/lib-es/logic/getStakes.d.ts.map +1 -1
  122. package/lib-es/logic/getStakes.js +9 -6
  123. package/lib-es/logic/getStakes.js.map +1 -1
  124. package/lib-es/logic/lastBlock.d.ts +2 -1
  125. package/lib-es/logic/lastBlock.d.ts.map +1 -1
  126. package/lib-es/logic/lastBlock.js +4 -3
  127. package/lib-es/logic/lastBlock.js.map +1 -1
  128. package/lib-es/logic/listOperations.d.ts +2 -1
  129. package/lib-es/logic/listOperations.d.ts.map +1 -1
  130. package/lib-es/logic/listOperations.js +7 -5
  131. package/lib-es/logic/listOperations.js.map +1 -1
  132. package/lib-es/logic/tezosToolkit.d.ts +2 -1
  133. package/lib-es/logic/tezosToolkit.d.ts.map +1 -1
  134. package/lib-es/logic/tezosToolkit.js +2 -3
  135. package/lib-es/logic/tezosToolkit.js.map +1 -1
  136. package/lib-es/logic/validateIntent.d.ts +2 -1
  137. package/lib-es/logic/validateIntent.d.ts.map +1 -1
  138. package/lib-es/logic/validateIntent.js +10 -8
  139. package/lib-es/logic/validateIntent.js.map +1 -1
  140. package/lib-es/network/bakers.d.ts +5 -4
  141. package/lib-es/network/bakers.d.ts.map +1 -1
  142. package/lib-es/network/bakers.js +9 -10
  143. package/lib-es/network/bakers.js.map +1 -1
  144. package/lib-es/network/index.d.ts +1 -1
  145. package/lib-es/network/index.d.ts.map +1 -1
  146. package/lib-es/network/index.js +1 -1
  147. package/lib-es/network/index.js.map +1 -1
  148. package/lib-es/network/tzkt.d.ts +16 -9
  149. package/lib-es/network/tzkt.d.ts.map +1 -1
  150. package/lib-es/network/tzkt.js +340 -333
  151. package/lib-es/network/tzkt.js.map +1 -1
  152. package/lib-es/utils.d.ts +2 -1
  153. package/lib-es/utils.d.ts.map +1 -1
  154. package/lib-es/utils.js +1 -3
  155. package/lib-es/utils.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/api/index-mainnet.integ.test.ts +34 -30
  158. package/src/api/index.integ.test.ts +69 -53
  159. package/src/api/index.test.ts +99 -60
  160. package/src/api/index.ts +52 -36
  161. package/src/api/types.ts +3 -2
  162. package/src/config.ts +4 -1
  163. package/src/logic/broadcast.integ.test.ts +12 -13
  164. package/src/logic/broadcast.ts +4 -2
  165. package/src/logic/combine.test.ts +17 -0
  166. package/src/logic/combine.ts +5 -2
  167. package/src/logic/craftRawOperations.test.ts +47 -33
  168. package/src/logic/craftRawOperations.ts +6 -4
  169. package/src/logic/craftTransaction.integ.test.ts +12 -5
  170. package/src/logic/craftTransaction.test.ts +39 -31
  171. package/src/logic/craftTransaction.ts +10 -5
  172. package/src/logic/estimateFees.integ.test.ts +7 -7
  173. package/src/logic/estimateFees.test.ts +52 -49
  174. package/src/logic/estimateFees.ts +34 -22
  175. package/src/logic/getBalance.test.ts +19 -17
  176. package/src/logic/getBalance.ts +6 -3
  177. package/src/logic/getBlock.integ.test.ts +8 -30
  178. package/src/logic/getBlock.test.ts +91 -81
  179. package/src/logic/getBlock.ts +11 -8
  180. package/src/logic/getBlockInfo.integ.test.ts +9 -18
  181. package/src/logic/getBlockInfo.test.ts +19 -9
  182. package/src/logic/getBlockInfo.ts +5 -2
  183. package/src/logic/getStakes.test.ts +46 -49
  184. package/src/logic/getStakes.ts +14 -5
  185. package/src/logic/lastBlock.ts +5 -3
  186. package/src/logic/listOperations.integ.test.ts +40 -67
  187. package/src/logic/listOperations.test.ts +65 -59
  188. package/src/logic/listOperations.ts +8 -3
  189. package/src/logic/tezosToolkit.ts +3 -3
  190. package/src/logic/validateIntent.test.ts +78 -74
  191. package/src/logic/validateIntent.ts +17 -7
  192. package/src/network/bakers.integ.test.ts +2 -5
  193. package/src/network/bakers.test.ts +5 -6
  194. package/src/network/bakers.ts +13 -10
  195. package/src/network/index.ts +2 -1
  196. package/src/network/tzkt.integ.test.ts +3 -14
  197. package/src/network/tzkt.test.ts +128 -96
  198. package/src/network/tzkt.ts +426 -393
  199. package/src/utils.ts +2 -3
package/src/api/index.ts CHANGED
@@ -1,7 +1,7 @@
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 { CoreAccountInfo, CoreTransactionInfo, EstimatedFees } from '../logic/estimateFees'
6
6
  import type { TezosOperationMode } from '../types/model'
7
7
  import type { TezosAccountInfo, TezosApi, TezosFeeEstimation } from './types'
@@ -24,7 +24,6 @@ import { craftTransactionData } from '@ledgerhq/coin-module-framework/logic/craf
24
24
  import { log } from '@ledgerhq/logs'
25
25
  import { getRevealFee } from '@taquito/taquito'
26
26
  import { getPkhfromPk, validatePublicKey, ValidationResult } from '@taquito/utils'
27
- import coinConfig from '../config'
28
27
  import {
29
28
  broadcast,
30
29
  combine,
@@ -43,7 +42,7 @@ import {
43
42
  import { getTezosToolkit } from '../logic/tezosToolkit'
44
43
  import { validateAddress } from '../logic/validateAddress'
45
44
  import { hasManagerKey } from '../network/types'
46
- import api from '../network/tzkt'
45
+ import { createTzktApi } from '../network/tzkt'
47
46
  import {
48
47
  DUST_MARGIN_MUTEZ,
49
48
  hasEmptyBalance,
@@ -52,53 +51,56 @@ import {
52
51
  resolveTezosOperationMode,
53
52
  } from '../utils'
54
53
 
55
- export function createApi(config: TezosConfig): TezosApi {
56
- coinConfig.setCoinConfig(() => ({ ...config, status: { type: 'active' } }))
57
-
54
+ // The caller builds the {@link TezosContext} (config + logger) and passes it to each method (ADR-019).
55
+ export function createApi(): TezosApi {
58
56
  return {
59
- broadcast,
57
+ broadcast: (context, tx) => broadcast(context, tx),
60
58
  async call() {
61
59
  throw new Error('call is not supported')
62
60
  },
63
- combine,
64
- craftTransaction: craft,
61
+ combine: (_context, tx, signature) => combine(tx, signature),
62
+ craftTransaction: (context, transactionIntent, options) =>
63
+ craft(context, transactionIntent, options?.customFees),
65
64
  craftRawTransaction: async (
65
+ context,
66
66
  transaction: string,
67
67
  sender: string,
68
68
  publicKey: string,
69
69
  sequence: bigint
70
70
  ): Promise<CraftedTransaction> => {
71
- const tx = await craftRawOperations(transaction, sender, publicKey, sequence)
71
+ const tx = await craftRawOperations(context, transaction, sender, publicKey, sequence)
72
72
  return { transaction: tx }
73
73
  },
74
- estimateFees: estimate,
75
- getBalance: (address: string, options?: BalanceOptions) =>
76
- rejectBalanceOptions(() => getBalance(address), options),
77
- lastBlock,
78
- listOperations: operations,
79
- getStakes,
80
- validateIntent,
81
- getNextSequence: async (address: string) => {
82
- const accountInfo = await api.getAccountByAddress(address)
74
+ estimateFees: (context, transactionIntent) => estimate(context, transactionIntent),
75
+ getBalance: (context, address: string, options?: BalanceOptions) =>
76
+ rejectBalanceOptions(() => getBalance(context, address), options),
77
+ lastBlock: (context) => lastBlock(context),
78
+ listOperations: (context, address, options) => operations(context, address, options),
79
+ getStakes: (context, address, options) => getStakes(context, address, options?.cursor),
80
+ validateIntent: (context, transactionIntent) => validateIntent(context, transactionIntent),
81
+ getNextSequence: async (context, address: string) => {
82
+ const config = await context.config()
83
+ const accountInfo = await createTzktApi(config).getAccountByAddress(address)
83
84
  return hasManagerKey(accountInfo) ? BigInt(accountInfo.counter + 1) : 0n
84
85
  },
85
- getAccountInfo: async (address: string): Promise<TezosAccountInfo> => {
86
- const account = await api.getAccountByAddress(address)
86
+ getAccountInfo: async (context, address: string): Promise<TezosAccountInfo> => {
87
+ const config = await context.config()
88
+ const account = await createTzktApi(config).getAccountByAddress(address)
87
89
  // Manager-key accounts (plain wallets "user" and registered bakers "delegate") carry a
88
90
  // reveal state; empty / non-existent accounts are treated as unrevealed (no public key
89
91
  // published on-chain yet).
90
92
  return { type: 'tezos', revealed: hasManagerKey(account) ? account.revealed : false }
91
93
  },
92
- getBlock,
93
- getBlockInfo,
94
- getRewards(_address: string, _cursor?: Cursor): Promise<Page<Reward>> {
94
+ getBlock: (context, height) => getBlock(context, height),
95
+ getBlockInfo: (context, height) => getBlockInfo(context, height),
96
+ getRewards(_context, _address: string, _options?: { cursor?: Cursor }): Promise<Page<Reward>> {
95
97
  throw new Error('getRewards is not supported')
96
98
  },
97
- getValidators(_cursor?: Cursor): Promise<Page<Validator>> {
99
+ getValidators(_context, _options?: { cursor?: Cursor }): Promise<Page<Validator>> {
98
100
  throw new Error('getValidators is not supported')
99
101
  },
100
- validateAddress,
101
- craftTransactionData,
102
+ validateAddress: (_context, address, parameters) => validateAddress(address, parameters),
103
+ craftTransactionData: (_context, intent) => craftTransactionData(intent),
102
104
  }
103
105
  }
104
106
 
@@ -108,6 +110,7 @@ function isTezosTransactionType(
108
110
  return ['send', 'delegate', 'undelegate', 'stake', 'unstake', 'finalize_unstake'].includes(type)
109
111
  }
110
112
  async function craft(
113
+ context: TezosContext,
111
114
  transactionIntent: TransactionIntent,
112
115
  customFees?: FeeEstimation
113
116
  ): Promise<CraftedTransaction> {
@@ -115,8 +118,11 @@ async function craft(
115
118
  throw new IncorrectTypeError(transactionIntent.type)
116
119
  }
117
120
 
121
+ const config = await context.config()
122
+ const api = createTzktApi(config)
123
+
118
124
  // Always estimate to get gasLimit/storageLimit
119
- const estimation = await estimate(transactionIntent)
125
+ const estimation = await estimate(context, transactionIntent)
120
126
  const fee = {
121
127
  fees: (customFees?.value ?? estimation.value).toString(),
122
128
  gasLimit: estimation.parameters?.gasLimit?.toString(),
@@ -156,7 +162,7 @@ async function craft(
156
162
  const needsReveal = hasManagerKey(senderApiAcc) && !senderApiAcc.revealed
157
163
  const totalFee = Number(fee.fees || '0')
158
164
 
159
- const feesConfig = coinConfig.getCoinConfig().fees
165
+ const feesConfig = config.fees
160
166
  const revealFeeForSplit = needsReveal
161
167
  ? Math.max(feesConfig.minFees ?? 0, getRevealFee(transactionIntent.sender))
162
168
  : 0
@@ -207,14 +213,23 @@ async function craft(
207
213
  return { publicKey: pk, publicKeyHash: transactionIntent.sender }
208
214
  })()
209
215
  : undefined
210
- const { contents } = await craftTransaction(accountForCraft, txForCraft, publicKeyForCraft)
211
- const tx = await rawEncode(contents)
216
+ const { contents } = await craftTransaction(
217
+ context,
218
+ accountForCraft,
219
+ txForCraft,
220
+ publicKeyForCraft
221
+ )
222
+ const tx = await rawEncode(config, contents)
212
223
  return { transaction: tx }
213
224
  }
214
225
 
215
- async function estimate(transactionIntent: TransactionIntent): Promise<TezosFeeEstimation> {
226
+ async function estimate(
227
+ context: TezosContext,
228
+ transactionIntent: TransactionIntent
229
+ ): Promise<TezosFeeEstimation> {
216
230
  // avoid taquito error when estimating a 0-amount transfer during input
217
- const config = coinConfig.getCoinConfig()
231
+ const config = await context.config()
232
+ const api = createTzktApi(config)
218
233
  const tezosModeForEstimate = resolveTezosOperationMode(
219
234
  transactionIntent.type,
220
235
  transactionIntent.asset
@@ -276,7 +291,7 @@ async function estimate(transactionIntent: TransactionIntent): Promise<TezosFeeE
276
291
  // needed by the compiler (it can assume it's a manager-key account with respective fields)
277
292
  if (!hasManagerKey(senderAccountInfo)) throw new Error('unexpected account type')
278
293
  const account = xpub ? { ...accountBase, xpub } : accountBase
279
- return await estimateFees({ account, transaction })
294
+ return await estimateFees(context, { account, transaction })
280
295
  }
281
296
  const xpub = transactionIntent.senderPublicKey ?? senderAccountInfo.publicKey
282
297
 
@@ -332,7 +347,7 @@ async function estimate(transactionIntent: TransactionIntent): Promise<TezosFeeE
332
347
  let txGasLimit: bigint
333
348
 
334
349
  try {
335
- const toolkit = getTezosToolkit()
350
+ const toolkit = getTezosToolkit(config)
336
351
  const simpleEstimate = await toolkit.estimate.transfer({
337
352
  to: transactionIntent.recipient,
338
353
  amount: Number(transactionIntent.amount),
@@ -382,13 +397,14 @@ async function estimate(transactionIntent: TransactionIntent): Promise<TezosFeeE
382
397
  }
383
398
 
384
399
  async function operations(
400
+ context: TezosContext,
385
401
  address: string,
386
402
  { minHeight = 0, cursor, order = 'asc' }: ListOperationsOptions
387
403
  ): Promise<Page<Operation>> {
388
404
  // FIXME This wrapper hard-codes limit: 1000 and ignores any caller-provided limit from ListOperationsOptions. Either
389
405
  // forward options.limit (as a soft/capped limit) or throw a "not supported" error when limit is set to match the
390
406
  // ListOperationsOptions contract.
391
- const [items, newNextCursor] = await listOperations(address, {
407
+ const [items, newNextCursor] = await listOperations(context, address, {
392
408
  limit: 1000, // Increased limit to 1000 to ensure delegation information is available when displaying account details (temporary fix until proper pagination is implemented).
393
409
  token: cursor,
394
410
  sort: order === 'asc' ? 'Ascending' : 'Descending',
package/src/api/types.ts CHANGED
@@ -1,6 +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
5
  import type { CoinModuleApi, FeeEstimation } from '@ledgerhq/coin-module-framework/api/types'
5
6
 
6
7
  export type TezosFeeParameters = {
@@ -32,6 +33,6 @@ export type TezosAccountInfo = { type: 'tezos'; revealed: boolean }
32
33
  * signatures would otherwise intersect into an incompatible-return-type collision) and letting the
33
34
  * implementation satisfy the generic method directly.
34
35
  */
35
- export type TezosApi = CoinModuleApi & {
36
- getAccountInfo: (address: string) => Promise<TezosAccountInfo>
36
+ export type TezosApi = CoinModuleApi<TezosCoinConfig> & {
37
+ getAccountInfo: (context: TezosContext, address: string) => Promise<TezosAccountInfo>
37
38
  }
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { CoinConfig, CurrencyConfig } from '@ledgerhq/coin-module-framework/config'
4
+ import type { CoinConfig, Context, CurrencyConfig } from '@ledgerhq/coin-module-framework/config'
5
5
  import buildCoinConfig from '@ledgerhq/coin-module-framework/config'
6
6
 
7
7
  export type TezosConfig = {
@@ -26,6 +26,9 @@ export type TezosConfig = {
26
26
 
27
27
  export type TezosCoinConfig = CurrencyConfig & TezosConfig
28
28
 
29
+ /** The {@link Context} threaded through the coin-tezos low layers (ADR-019). */
30
+ export type TezosContext = Context<TezosCoinConfig>
31
+
29
32
  const coinConfig: {
30
33
  setCoinConfig: (config: CoinConfig<TezosCoinConfig>) => void
31
34
  getCoinConfig: (currencyId?: string) => TezosCoinConfig
@@ -1,23 +1,22 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import type { TezosContext } from '../config'
4
5
  import { InMemorySigner } from '@taquito/signer'
5
6
  import { OpKind, TezosToolkit } from '@taquito/taquito'
6
- import coinConfig from '../config'
7
7
  import { broadcast } from './broadcast'
8
8
 
9
9
  describe('Broadcast', () => {
10
- beforeAll(() => {
11
- coinConfig.setCoinConfig(
12
- () =>
13
- ({
14
- status: { type: 'active' },
15
- node: {
16
- url: 'https://xtz-node.api.live.ledger.com',
17
- },
18
- }) as any
19
- )
20
- })
10
+ const context: TezosContext = {
11
+ config: async () =>
12
+ ({
13
+ status: { type: 'active' },
14
+ node: {
15
+ url: 'https://xtz-node.api.live.ledger.com',
16
+ },
17
+ }) as any,
18
+ logger: () => undefined,
19
+ }
21
20
 
22
21
  it('throws on insufficient funds', async () => {
23
22
  const tezos = new TezosToolkit('https://xtz-node.api.live.ledger.com')
@@ -47,6 +46,6 @@ describe('Broadcast', () => {
47
46
  })
48
47
  const { sbytes } = await signer.sign(prepared, new Uint8Array([3]))
49
48
 
50
- await expect(broadcast(sbytes)).rejects.toThrow(/Empty implicit contract/)
49
+ await expect(broadcast(context, sbytes)).rejects.toThrow(/Empty implicit contract/)
51
50
  })
52
51
  })
@@ -1,9 +1,11 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import type { TezosContext } from '../config'
4
5
  import { getTezosToolkit } from './tezosToolkit'
5
6
 
6
- export async function broadcast(signature: string): Promise<string> {
7
- const { rpc } = getTezosToolkit()
7
+ export async function broadcast(context: TezosContext, signature: string): Promise<string> {
8
+ const config = await context.config()
9
+ const { rpc } = getTezosToolkit(config)
8
10
  return await rpc.injectOperation(signature)
9
11
  }
@@ -0,0 +1,17 @@
1
+ // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { combine } from './combine'
5
+
6
+ describe('combine', () => {
7
+ it('concatenates transaction and signature and strips the leading 2 chars', () => {
8
+ // slice(2) removes the first byte (magic byte) from the concatenated hex string
9
+ expect(combine('03aabbcc', ['ddeeff'])).toBe('aabbccddeeff')
10
+ })
11
+
12
+ it('throws when more than one signature is provided', () => {
13
+ expect(() => combine('0xdeadbeef', ['0xsig0', '0xsig1'])).toThrow(
14
+ 'Tezos combine expects exactly one signature, got 2'
15
+ )
16
+ })
17
+ })
@@ -2,6 +2,9 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  // Source: https://github.com/ecadlabs/taquito/blob/97832fc088eccb93c152659367618ce1782a8b51/packages/taquito-ledger-signer/src/taquito-ledger-signer.ts#L187C23-L187C32
5
- export function combine(transaction: string, signature: string): string {
6
- return `${transaction}${signature}`.slice(2)
5
+ export function combine(transaction: string, signature: string[]): string {
6
+ if (signature.length !== 1) {
7
+ throw new Error(`Tezos combine expects exactly one signature, got ${signature.length}`)
8
+ }
9
+ return `${transaction}${signature[0]}`.slice(2)
7
10
  }
@@ -1,17 +1,14 @@
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
5
  import { OpKind } from '@taquito/rpc'
5
6
  import { getPkhfromPk } from '@taquito/utils'
6
- import coinConfig from '../config'
7
7
  import { UnsupportedOperationKind } from '../types/errors'
8
8
  import { craftRawOperations } from './craftRawOperations'
9
9
  import { getTezosToolkit } from './tezosToolkit'
10
10
 
11
11
  jest.mock('./tezosToolkit')
12
- jest.mock('../config', () => ({
13
- getCoinConfig: jest.fn(),
14
- }))
15
12
  // Keep real validatePublicKey/normalize, override only the pkh derivation so we can
16
13
  // pair an arbitrary valid edpk with our test sender in the reveal path.
17
14
  jest.mock('@taquito/utils', () => ({
@@ -47,6 +44,14 @@ describe('craftRawOperations', () => {
47
44
  return mockTezosToolkit.rpc.forgeOperations.mock.calls[0][0].contents
48
45
  }
49
46
 
47
+ const context: TezosContext = {
48
+ config: async () =>
49
+ ({
50
+ fees: { minFees: 100, minRevealGasLimit: 0, minStorageLimit: 0 },
51
+ }) as unknown as TezosCoinConfig,
52
+ logger: () => undefined,
53
+ }
54
+
50
55
  beforeEach(() => {
51
56
  jest.clearAllMocks()
52
57
  ;(getTezosToolkit as jest.Mock).mockReturnValue(mockTezosToolkit)
@@ -58,15 +63,12 @@ describe('craftRawOperations', () => {
58
63
  mockTezosToolkit.estimate.batch.mockResolvedValue([
59
64
  { suggestedFeeMutez: 500, gasLimit: 1400, storageLimit: 0 },
60
65
  ])
61
- ;(coinConfig.getCoinConfig as jest.Mock).mockReturnValue({
62
- fees: { minFees: 100, minRevealGasLimit: 0, minStorageLimit: 0 },
63
- })
64
66
  })
65
67
 
66
68
  it('forges a plain transfer, estimating the missing limits', async () => {
67
69
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
68
70
 
69
- const result = await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
71
+ const result = await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
70
72
 
71
73
  expect(result).toBe('03deadbeef') // 0x03 watermark + forged bytes
72
74
  expect(mockTezosToolkit.estimate.batch).toHaveBeenCalledWith([
@@ -90,7 +92,7 @@ describe('craftRawOperations', () => {
90
92
  const parameters = { entrypoint: 'transfer', value: { prim: 'Unit' } }
91
93
  const ops = [{ kind: 'transaction', destination: CONTRACT, amount: '0', parameters }]
92
94
 
93
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 7n)
95
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 7n)
94
96
 
95
97
  expect(mockTezosToolkit.estimate.batch).toHaveBeenCalledWith([
96
98
  {
@@ -116,7 +118,7 @@ describe('craftRawOperations', () => {
116
118
  ])
117
119
  const ops = [{ kind: 'delegation', delegate: BAKER }]
118
120
 
119
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 3n)
121
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 3n)
120
122
 
121
123
  expect(mockTezosToolkit.estimate.batch).toHaveBeenCalledWith([
122
124
  { kind: OpKind.DELEGATION, source: SENDER, delegate: BAKER },
@@ -149,7 +151,7 @@ describe('craftRawOperations', () => {
149
151
  },
150
152
  ]
151
153
 
152
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 9n)
154
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 9n)
153
155
 
154
156
  expect(mockTezosToolkit.estimate.batch).toHaveBeenCalledTimes(1)
155
157
  const contents = forgedContents()
@@ -173,7 +175,7 @@ describe('craftRawOperations', () => {
173
175
  ])
174
176
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
175
177
 
176
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
178
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
177
179
 
178
180
  const contents = forgedContents()
179
181
  expect(contents[0].kind).toBe(OpKind.REVEAL)
@@ -193,9 +195,9 @@ describe('craftRawOperations', () => {
193
195
  ])
194
196
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
195
197
 
196
- await expect(craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
197
- 'expected 1 estimate(s)'
198
- )
198
+ await expect(
199
+ craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
200
+ ).rejects.toThrow('expected 1 estimate(s)')
199
201
  })
200
202
 
201
203
  it('honours caller-provided fee/gas/storage without estimating', async () => {
@@ -210,7 +212,7 @@ describe('craftRawOperations', () => {
210
212
  },
211
213
  ]
212
214
 
213
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
215
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
214
216
 
215
217
  expect(mockTezosToolkit.estimate.batch).not.toHaveBeenCalled()
216
218
  expect(forgedContents()[0]).toMatchObject({
@@ -231,7 +233,7 @@ describe('craftRawOperations', () => {
231
233
 
232
234
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
233
235
 
234
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
236
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
235
237
 
236
238
  const contents = forgedContents()
237
239
  expect(contents).toHaveLength(2)
@@ -251,7 +253,7 @@ describe('craftRawOperations', () => {
251
253
 
252
254
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
253
255
 
254
- await craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
256
+ await craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
255
257
 
256
258
  const reveal = forgedContents()[0]
257
259
  expect(reveal.kind).toBe(OpKind.REVEAL)
@@ -266,39 +268,40 @@ describe('craftRawOperations', () => {
266
268
 
267
269
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
268
270
 
269
- await expect(craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
270
- 'does not match the sender'
271
- )
271
+ await expect(
272
+ craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
273
+ ).rejects.toThrow('does not match the sender')
272
274
  })
273
275
 
274
276
  it('throws on an unsupported operation kind', async () => {
275
277
  const ops = [{ kind: 'origination', balance: '0' }]
276
278
 
277
279
  await expect(
278
- craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
280
+ craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
279
281
  ).rejects.toBeInstanceOf(UnsupportedOperationKind)
280
282
  })
281
283
 
282
284
  describe('input validation', () => {
283
285
  it('throws on malformed JSON or an empty array', async () => {
284
- await expect(craftRawOperations('not-json', SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
286
+ await expect(craftRawOperations(context, 'not-json', SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
285
287
  'must be a JSON array'
286
288
  )
287
- await expect(craftRawOperations('[]', SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
289
+ await expect(craftRawOperations(context, '[]', SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
288
290
  'non-empty array'
289
291
  )
290
292
  })
291
293
 
292
294
  it('throws on a non-integer amount', async () => {
293
295
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1.5' }]
294
- await expect(craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
295
- '`amount` must be a non-negative integer string'
296
- )
296
+ await expect(
297
+ craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
298
+ ).rejects.toThrow('`amount` must be a non-negative integer string')
297
299
  })
298
300
 
299
301
  it('throws on a missing or invalid destination', async () => {
300
302
  await expect(
301
303
  craftRawOperations(
304
+ context,
302
305
  JSON.stringify([{ kind: 'transaction', amount: '1000' }]),
303
306
  SENDER,
304
307
  PUBLIC_KEY,
@@ -307,6 +310,7 @@ describe('craftRawOperations', () => {
307
310
  ).rejects.toThrow('valid `destination`')
308
311
  await expect(
309
312
  craftRawOperations(
313
+ context,
310
314
  JSON.stringify([{ kind: 'transaction', destination: 'not-an-address', amount: '1000' }]),
311
315
  SENDER,
312
316
  PUBLIC_KEY,
@@ -317,14 +321,15 @@ describe('craftRawOperations', () => {
317
321
 
318
322
  it('throws on a non-numeric fee', async () => {
319
323
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000', fee: 'abc' }]
320
- await expect(craftRawOperations(JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)).rejects.toThrow(
321
- '`fee` must be a non-negative integer string'
322
- )
324
+ await expect(
325
+ craftRawOperations(context, JSON.stringify(ops), SENDER, PUBLIC_KEY, 5n)
326
+ ).rejects.toThrow('`fee` must be a non-negative integer string')
323
327
  })
324
328
 
325
329
  it('rejects an empty or invalid delegate (must not be treated as undelegation)', async () => {
326
330
  await expect(
327
331
  craftRawOperations(
332
+ context,
328
333
  JSON.stringify([{ kind: 'delegation', delegate: '' }]),
329
334
  SENDER,
330
335
  PUBLIC_KEY,
@@ -333,6 +338,7 @@ describe('craftRawOperations', () => {
333
338
  ).rejects.toThrow('`delegate` must be a valid implicit account address')
334
339
  await expect(
335
340
  craftRawOperations(
341
+ context,
336
342
  JSON.stringify([{ kind: 'delegation', delegate: 123 }]),
337
343
  SENDER,
338
344
  PUBLIC_KEY,
@@ -344,6 +350,7 @@ describe('craftRawOperations', () => {
344
350
  it('rejects a contract (KT1) delegate (delegates must be implicit accounts)', async () => {
345
351
  await expect(
346
352
  craftRawOperations(
353
+ context,
347
354
  JSON.stringify([{ kind: 'delegation', delegate: CONTRACT }]),
348
355
  SENDER,
349
356
  PUBLIC_KEY,
@@ -356,7 +363,13 @@ describe('craftRawOperations', () => {
356
363
  mockTezosToolkit.estimate.batch.mockResolvedValue([
357
364
  { suggestedFeeMutez: 400, gasLimit: 1000, storageLimit: 0 },
358
365
  ])
359
- await craftRawOperations(JSON.stringify([{ kind: 'delegation' }]), SENDER, PUBLIC_KEY, 5n)
366
+ await craftRawOperations(
367
+ context,
368
+ JSON.stringify([{ kind: 'delegation' }]),
369
+ SENDER,
370
+ PUBLIC_KEY,
371
+ 5n
372
+ )
360
373
 
361
374
  const content = forgedContents()[0]
362
375
  expect(content.kind).toBe(OpKind.DELEGATION)
@@ -367,6 +380,7 @@ describe('craftRawOperations', () => {
367
380
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
368
381
  await expect(
369
382
  craftRawOperations(
383
+ context,
370
384
  JSON.stringify(ops),
371
385
  SENDER,
372
386
  PUBLIC_KEY,
@@ -378,10 +392,10 @@ describe('craftRawOperations', () => {
378
392
  it('throws on an invalid or contract (KT1) sender address', async () => {
379
393
  const ops = [{ kind: 'transaction', destination: RECIPIENT, amount: '1000' }]
380
394
  await expect(
381
- craftRawOperations(JSON.stringify(ops), 'not-an-address', PUBLIC_KEY, 5n)
395
+ craftRawOperations(context, JSON.stringify(ops), 'not-an-address', PUBLIC_KEY, 5n)
382
396
  ).rejects.toThrow('`sender` must be a valid implicit account address')
383
397
  await expect(
384
- craftRawOperations(JSON.stringify(ops), CONTRACT, PUBLIC_KEY, 5n)
398
+ craftRawOperations(context, JSON.stringify(ops), CONTRACT, PUBLIC_KEY, 5n)
385
399
  ).rejects.toThrow('`sender` must be a valid implicit account address')
386
400
  })
387
401
  })
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import type { TezosContext } from '../config'
4
5
  import type { MichelsonV1Expression, OperationContents } from '@taquito/rpc'
5
6
  import type { Estimate, ParamsWithKind } from '@taquito/taquito'
6
7
  import { OpKind } from '@taquito/rpc'
@@ -11,7 +12,6 @@ import {
11
12
  validatePublicKey,
12
13
  ValidationResult,
13
14
  } from '@taquito/utils'
14
- import coinConfig from '../config'
15
15
  import { UnsupportedOperationKind } from '../types/errors'
16
16
  import { createMockSigner, normalizePublicKeyForAddress } from '../utils'
17
17
  import { rawEncode } from './craftTransaction'
@@ -61,11 +61,13 @@ type ResolvedLimits = { fee: string; gasLimit: string; storageLimit: string }
61
61
  * `signer.signTransaction` + `combine` + broadcast path applies to it unchanged.
62
62
  */
63
63
  export async function craftRawOperations(
64
+ context: TezosContext,
64
65
  rawTransaction: string,
65
66
  sender: string,
66
67
  publicKey: string,
67
68
  sequence: bigint
68
69
  ): Promise<string> {
70
+ const config = await context.config()
69
71
  if (sequence < 0n || sequence > BigInt(Number.MAX_SAFE_INTEGER)) {
70
72
  throw new Error('craftRawOperations: sequence is out of the safe integer range')
71
73
  }
@@ -74,12 +76,12 @@ export async function craftRawOperations(
74
76
  }
75
77
  const operations = parseOperations(rawTransaction)
76
78
 
77
- const tezosToolkit = getTezosToolkit()
79
+ const tezosToolkit = getTezosToolkit(config)
78
80
  const normalizedPk = normalizePublicKeyForAddress(publicKey, sender)
79
81
  // Mock signer lets Taquito estimate without a connected device.
80
82
  tezosToolkit.setProvider({ signer: createMockSigner(sender, normalizedPk ?? '') })
81
83
 
82
- const feesConfig = coinConfig.getCoinConfig().fees
84
+ const feesConfig = config.fees
83
85
  const minFees = feesConfig.minFees
84
86
 
85
87
  let counter = Number(sequence)
@@ -115,7 +117,7 @@ export async function craftRawOperations(
115
117
  counter += 1
116
118
  })
117
119
 
118
- return rawEncode(contents)
120
+ return rawEncode(config, contents)
119
121
  }
120
122
 
121
123
  function parseOperations(rawTransaction: string): PartialTezosOperation[] {