@ledgerhq/coin-tezos 9.0.0 → 10.1.1

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.
@@ -2,7 +2,6 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import type { TezosCoinConfig, TezosConfig, TezosContext } from '../config'
5
- import type { TezosApi } from './types'
6
5
  import { localForger } from '@taquito/local-forging'
7
6
  import { OpKind } from '@taquito/rpc'
8
7
  import { createTzktApi } from '../network/tzkt'
@@ -55,7 +54,7 @@ type DecodedOperation = {
55
54
  * Use this test suite in last resort
56
55
  */
57
56
  describe('Tezos Api - Mainnet', () => {
58
- const module: TezosApi = createApi()
57
+ const module = createApi()
59
58
  const context: TezosContext = {
60
59
  config: async () => defaultConfig as TezosCoinConfig,
61
60
  logger: () => undefined,
@@ -2,7 +2,6 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import type { TezosCoinConfig, TezosConfig, TezosContext } from '../config'
5
- import type { TezosApi } from './types'
6
5
  import type { SendTransactionIntent } from '@ledgerhq/coin-module-framework/api/types'
7
6
  import { localForger } from '@taquito/local-forging'
8
7
  import { createApi } from '.'
@@ -26,7 +25,7 @@ const defaultConfig: TezosConfig = {
26
25
  * https://api.shadownet.tzkt.io
27
26
  */
28
27
  describe('Tezos Api', () => {
29
- const module: TezosApi = createApi()
28
+ const module = createApi()
30
29
  const context: TezosContext = {
31
30
  config: async () => defaultConfig as TezosCoinConfig,
32
31
  logger: () => undefined,
@@ -995,6 +995,8 @@ describe('getBalance', () => {
995
995
  })
996
996
 
997
997
  describe('getAccountInfo', () => {
998
+ // `getAccountInfo` is optional on the generic CoinModuleApi (ADR-045 / LIVE-33431); coin-tezos
999
+ // always implements it, so the non-null assertions below are safe.
998
1000
  afterEach(() => {
999
1001
  mockGetAccountByAddress.mockClear()
1000
1002
  })
@@ -1013,7 +1015,7 @@ describe('getAccountInfo', () => {
1013
1015
  firstActivityTime: '2021-01-01T00:00:00Z',
1014
1016
  } as APIAccount)
1015
1017
 
1016
- await expect(api.getAccountInfo(context, 'tz1test')).resolves.toEqual({
1018
+ await expect(api.getAccountInfo!(context, 'tz1test')).resolves.toEqual({
1017
1019
  type: 'tezos',
1018
1020
  revealed: true,
1019
1021
  })
@@ -1034,7 +1036,7 @@ describe('getAccountInfo', () => {
1034
1036
  firstActivityTime: '2021-01-01T00:00:00Z',
1035
1037
  } as APIAccount)
1036
1038
 
1037
- await expect(api.getAccountInfo(context, 'tz2unrevealed')).resolves.toEqual({
1039
+ await expect(api.getAccountInfo!(context, 'tz2unrevealed')).resolves.toEqual({
1038
1040
  type: 'tezos',
1039
1041
  revealed: false,
1040
1042
  })
@@ -1054,7 +1056,7 @@ describe('getAccountInfo', () => {
1054
1056
  } as APIAccount)
1055
1057
 
1056
1058
  await expect(
1057
- api.getAccountInfo(context, 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9')
1059
+ api.getAccountInfo!(context, 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9')
1058
1060
  ).resolves.toEqual({
1059
1061
  type: 'tezos',
1060
1062
  revealed: true,
@@ -1069,7 +1071,7 @@ describe('getAccountInfo', () => {
1069
1071
  counter: 0,
1070
1072
  } as APIAccount)
1071
1073
 
1072
- await expect(api.getAccountInfo(context, 'tz1empty')).resolves.toEqual({
1074
+ await expect(api.getAccountInfo!(context, 'tz1empty')).resolves.toEqual({
1073
1075
  type: 'tezos',
1074
1076
  revealed: false,
1075
1077
  })
@@ -1079,6 +1081,6 @@ describe('getAccountInfo', () => {
1079
1081
  it('propagates errors from getAccountByAddress', async () => {
1080
1082
  mockGetAccountByAddress.mockRejectedValueOnce(new Error('boom'))
1081
1083
 
1082
- await expect(api.getAccountInfo(context, 'tz1boom')).rejects.toThrow('boom')
1084
+ await expect(api.getAccountInfo!(context, 'tz1boom')).rejects.toThrow('boom')
1083
1085
  })
1084
1086
  })
package/src/api/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // SPDX-FileCopyrightText: © 2026 LEDGER SAS
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import type { TezosContext } from '../config'
4
+ import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import type { CoreAccountInfo, CoreTransactionInfo, EstimatedFees } from '../logic/estimateFees'
6
6
  import type { TezosOperationMode } from '../types/model'
7
- import type { TezosAccountInfo, TezosApi, TezosFeeEstimation } from './types'
7
+ import type { TezosAccountInfo, TezosFeeEstimation } from './types'
8
8
  import type {
9
9
  CraftedTransaction,
10
10
  Cursor,
@@ -15,11 +15,16 @@ import type {
15
15
  } from '@ledgerhq/coin-module-framework/api/index'
16
16
  import type {
17
17
  BalanceOptions,
18
+ CoinModuleApi,
18
19
  FeeEstimation,
19
20
  TransactionIntent,
20
21
  } from '@ledgerhq/coin-module-framework/api/types'
21
22
  import { rejectBalanceOptions } from '@ledgerhq/coin-module-framework/api/getBalance/rejectBalanceOptions'
22
- import { IncorrectTypeError, type Operation } from '@ledgerhq/coin-module-framework/api/index'
23
+ import {
24
+ IncorrectTypeError,
25
+ notSupported,
26
+ type Operation,
27
+ } from '@ledgerhq/coin-module-framework/api/index'
23
28
  import { craftTransactionData } from '@ledgerhq/coin-module-framework/logic/craftTransactionData'
24
29
  import { log } from '@ledgerhq/logs'
25
30
  import { getRevealFee } from '@taquito/taquito'
@@ -52,7 +57,7 @@ import {
52
57
  } from '../utils'
53
58
 
54
59
  // The caller builds the {@link TezosContext} (config + logger) and passes it to each method (ADR-019).
55
- export function createApi(): TezosApi {
60
+ export function createApi(): CoinModuleApi<TezosCoinConfig> {
56
61
  return {
57
62
  broadcast: (context, tx) => broadcast(context, tx),
58
63
  async call() {
@@ -96,6 +101,7 @@ export function createApi(): TezosApi {
96
101
  getRewards(_context, _address: string, _options?: { cursor?: Cursor }): Promise<Page<Reward>> {
97
102
  throw new Error('getRewards is not supported')
98
103
  },
104
+ register: notSupported('register'),
99
105
  getValidators(_context, _options?: { cursor?: Cursor }): Promise<Page<Validator>> {
100
106
  throw new Error('getValidators is not supported')
101
107
  },
package/src/api/types.ts CHANGED
@@ -1,8 +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'
5
- import type { CoinModuleApi, FeeEstimation } from '@ledgerhq/coin-module-framework/api/types'
4
+ import type { FeeEstimation } from '@ledgerhq/coin-module-framework/api/types'
6
5
 
7
6
  export type TezosFeeParameters = {
8
7
  gasLimit: bigint
@@ -19,20 +18,9 @@ export type TezosSender = { address: string; xpub?: string }
19
18
  /**
20
19
  * Network-specific account metadata for Tezos (ADR-045 shape).
21
20
  * `revealed` is `true` once the account has published its public key on-chain.
22
- */
23
- export type TezosAccountInfo = { type: 'tezos'; revealed: boolean }
24
-
25
- /**
26
- * coin-tezos public API surface: the generic {@link CoinModuleApi} plus `getAccountInfo`,
27
- * which exposes the account's on-chain reveal state.
28
21
  *
29
- * Kept local (rather than relying on the future framework-side reveal-state type from ADR-045)
30
- * so coin-tezos does not depend on that framework change landing first. The `{ type: "tezos" }`
31
- * shape is intentionally identical to the ADR-045 variant. Once the framework's `CoinModuleApi`
32
- * ships its own `getAccountInfo`, migrating means dropping this local override entirely (the two
33
- * signatures would otherwise intersect into an incompatible-return-type collision) and letting the
34
- * implementation satisfy the generic method directly.
22
+ * Concrete return type of the module's `getAccountInfo`. The framework's generic
23
+ * `CoinModuleApi.getAccountInfo` (added by ADR-045 / LIVE-33431) widens to the open
24
+ * `AccountInfo` shape; this alias keeps the reveal state strongly typed at the implementation.
35
25
  */
36
- export type TezosApi = CoinModuleApi<TezosCoinConfig> & {
37
- getAccountInfo: (context: TezosContext, address: string) => Promise<TezosAccountInfo>
38
- }
26
+ export type TezosAccountInfo = { type: 'tezos'; revealed: boolean }
@@ -3,10 +3,27 @@
3
3
 
4
4
  import type { TezosCoinConfig, TezosContext } from '../config'
5
5
  import type { APITokenBalance } from '../network/types'
6
- import { HttpResponse, http } from 'msw'
7
- import { setupServer } from 'msw/node'
8
6
  import { getBalance } from './getBalance'
9
7
 
8
+ const mockedGetAccountByAddress = jest.fn()
9
+ const mockedGetTokensBalances = jest.fn()
10
+ const mockedGetUnstakeRequests = jest.fn()
11
+
12
+ jest.mock('../network/tzkt', () => {
13
+ const originalModule = jest.requireActual('../network/tzkt')
14
+ return {
15
+ ...originalModule,
16
+ createTzktApi: () => {
17
+ return {
18
+ ...originalModule.api,
19
+ getAccountByAddress: mockedGetAccountByAddress,
20
+ getTokensBalances: mockedGetTokensBalances,
21
+ getUnstakeRequests: mockedGetUnstakeRequests,
22
+ }
23
+ },
24
+ }
25
+ })
26
+
10
27
  const mockTokensBalancesWithAllInfo: APITokenBalance[] = [
11
28
  {
12
29
  id: 2398642347442177,
@@ -51,14 +68,8 @@ const mockTokensBalancesWithMissingInfo: APITokenBalance[] = [
51
68
  ]
52
69
 
53
70
  describe('getBalance', () => {
54
- const mockServer = setupServer()
55
-
56
- beforeAll(() => {
57
- mockServer.listen({ onUnhandledRequest: 'error' })
58
- })
59
-
60
- afterAll(() => {
61
- mockServer.close()
71
+ beforeEach(() => {
72
+ jest.resetAllMocks()
62
73
  })
63
74
 
64
75
  const context: TezosContext = {
@@ -70,35 +81,31 @@ describe('getBalance', () => {
70
81
  logger: () => undefined,
71
82
  }
72
83
  it('gets the balance of a Tezos account', async () => {
73
- mockServer.use(
74
- http.get('http://tezos.explorer.com/v1/accounts/tz1WvvbEGpBXGeTVbLiR6DYBe1izmgiYuZbq', () =>
75
- HttpResponse.json({ type: 'empty' })
76
- ),
77
- http.get('http://tezos.explorer.com/v1/accounts/tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8', () =>
78
- HttpResponse.json({ type: 'user', balance: 25 })
79
- ),
80
- http.get('http://tezos.explorer.com/v1/accounts/tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM', () =>
81
- HttpResponse.json({
84
+ mockedGetAccountByAddress.mockImplementation((address: string) => {
85
+ if (address === 'tz1WvvbEGpBXGeTVbLiR6DYBe1izmgiYuZbq') {
86
+ return { type: 'empty' }
87
+ } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8') {
88
+ return { type: 'user', balance: 25 }
89
+ } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM') {
90
+ return {
82
91
  type: 'user',
83
92
  balance: 15,
84
93
  delegate: { address: 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM' },
85
- })
86
- ),
87
- http.get('http://tezos.explorer.com/v1/tokens/balances', ({ request }) => {
88
- const url = new URL(request.url)
89
- const account = url.searchParams.get('account')
90
-
91
- // Return a mocked JSON response when the "account"
92
- // search parameter equals a specific address.
93
- if (account === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8') {
94
- return HttpResponse.json(mockTokensBalancesWithAllInfo)
95
- } else if (account === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM') {
96
- return HttpResponse.json(mockTokensBalancesWithMissingInfo)
97
- } else {
98
- return HttpResponse.json([])
99
94
  }
100
- })
101
- )
95
+ } else {
96
+ throw new Error('address not handled by the test: ' + address)
97
+ }
98
+ })
99
+
100
+ mockedGetTokensBalances.mockImplementation((address: string) => {
101
+ if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oSM8') {
102
+ return mockTokensBalancesWithAllInfo
103
+ } else if (address === 'tz1TzrmTBSuiVHV2VfMnGRMYvTEPCP42oMMM') {
104
+ return mockTokensBalancesWithMissingInfo
105
+ } else {
106
+ return []
107
+ }
108
+ })
102
109
 
103
110
  expect(await getBalance(context, 'tz1WvvbEGpBXGeTVbLiR6DYBe1izmgiYuZbq')).toEqual([
104
111
  {
@@ -164,15 +171,9 @@ describe('getBalance', () => {
164
171
  const delegateAddress = 'tz1BakerAddr'
165
172
 
166
173
  function mockAccount(account: Record<string, unknown>) {
167
- mockServer.use(
168
- http.get(`http://tezos.explorer.com/v1/accounts/${address}`, () =>
169
- HttpResponse.json({ type: 'user', ...account })
170
- ),
171
- http.get('http://tezos.explorer.com/v1/tokens/balances', () => HttpResponse.json([])),
172
- http.get('http://tezos.explorer.com/v1/staking/unstake_requests', () =>
173
- HttpResponse.json([])
174
- )
175
- )
174
+ mockedGetAccountByAddress.mockResolvedValue({ type: 'user', ...account })
175
+ mockedGetTokensBalances.mockResolvedValue([])
176
+ mockedGetUnstakeRequests.mockResolvedValue([])
176
177
  }
177
178
 
178
179
  it('attaches a delegation Stake when only delegate is set', async () => {
@@ -271,31 +272,25 @@ describe('getBalance', () => {
271
272
  })
272
273
 
273
274
  it("attaches an unstaking Stake with state 'deactivating' when a pending request exists", async () => {
274
- mockServer.use(
275
- http.get(`http://tezos.explorer.com/v1/accounts/${address}`, () =>
276
- HttpResponse.json({
277
- type: 'user',
278
- balance: 100,
279
- stakedBalance: 30,
280
- unstakedBalance: 10,
281
- delegate: { address: delegateAddress },
282
- })
283
- ),
284
- http.get('http://tezos.explorer.com/v1/staking/unstake_requests', () =>
285
- HttpResponse.json([
286
- {
287
- id: 77,
288
- cycle: 100,
289
- baker: { address: delegateAddress },
290
- staker: { address },
291
- firstTime: '2026-05-01T00:00:00Z',
292
- status: 'pending',
293
- actualAmount: 10,
294
- },
295
- ])
296
- ),
297
- http.get('http://tezos.explorer.com/v1/tokens/balances', () => HttpResponse.json([]))
298
- )
275
+ mockedGetAccountByAddress.mockResolvedValue({
276
+ type: 'user',
277
+ balance: 100,
278
+ stakedBalance: 30,
279
+ unstakedBalance: 10,
280
+ delegate: { address: delegateAddress },
281
+ })
282
+ mockedGetTokensBalances.mockResolvedValue([])
283
+ mockedGetUnstakeRequests.mockResolvedValue([
284
+ {
285
+ id: 77,
286
+ cycle: 100,
287
+ baker: { address: delegateAddress },
288
+ staker: { address },
289
+ firstTime: '2026-05-01T00:00:00Z',
290
+ status: 'pending',
291
+ actualAmount: 10,
292
+ },
293
+ ])
299
294
 
300
295
  const result = await getBalance(context, address)
301
296
 
@@ -318,39 +313,33 @@ describe('getBalance', () => {
318
313
  })
319
314
 
320
315
  it('emits per-request unstaking and finalizable Stakes when both statuses are present', async () => {
321
- mockServer.use(
322
- http.get(`http://tezos.explorer.com/v1/accounts/${address}`, () =>
323
- HttpResponse.json({
324
- type: 'user',
325
- balance: 100,
326
- unstakedBalance: 50,
327
- delegate: { address: delegateAddress },
328
- })
329
- ),
330
- http.get('http://tezos.explorer.com/v1/staking/unstake_requests', () =>
331
- HttpResponse.json([
332
- {
333
- id: 1,
334
- cycle: 100,
335
- baker: { address: delegateAddress },
336
- staker: { address },
337
- firstTime: '2026-05-01T00:00:00Z',
338
- status: 'pending',
339
- actualAmount: 20,
340
- },
341
- {
342
- id: 2,
343
- cycle: 99,
344
- baker: { address: delegateAddress },
345
- staker: { address },
346
- firstTime: '2026-04-25T00:00:00Z',
347
- status: 'finalizable',
348
- actualAmount: 30,
349
- },
350
- ])
351
- ),
352
- http.get('http://tezos.explorer.com/v1/tokens/balances', () => HttpResponse.json([]))
353
- )
316
+ mockedGetAccountByAddress.mockResolvedValue({
317
+ type: 'user',
318
+ balance: 100,
319
+ unstakedBalance: 50,
320
+ delegate: { address: delegateAddress },
321
+ })
322
+ mockedGetUnstakeRequests.mockResolvedValue([
323
+ {
324
+ id: 1,
325
+ cycle: 100,
326
+ baker: { address: delegateAddress },
327
+ staker: { address },
328
+ firstTime: '2026-05-01T00:00:00Z',
329
+ status: 'pending',
330
+ actualAmount: 20,
331
+ },
332
+ {
333
+ id: 2,
334
+ cycle: 99,
335
+ baker: { address: delegateAddress },
336
+ staker: { address },
337
+ firstTime: '2026-04-25T00:00:00Z',
338
+ status: 'finalizable',
339
+ actualAmount: 30,
340
+ },
341
+ ])
342
+ mockedGetTokensBalances.mockResolvedValue([])
354
343
 
355
344
  const result = await getBalance(context, address)
356
345
 
@@ -396,30 +385,25 @@ describe('getBalance', () => {
396
385
  // A delegate has no `delegate` field (it is its own baker), so there is no delegation position —
397
386
  // only its self-stake and any unstake requests. The unstakedBalance > 0 exercises the
398
387
  // delegate → fetchUnstakeRequests path.
399
- mockServer.use(
400
- http.get(`http://tezos.explorer.com/v1/accounts/${address}`, () =>
401
- HttpResponse.json({
402
- type: 'delegate',
403
- balance: 100,
404
- stakedBalance: 30,
405
- unstakedBalance: 10,
406
- })
407
- ),
408
- http.get('http://tezos.explorer.com/v1/tokens/balances', () => HttpResponse.json([])),
409
- http.get('http://tezos.explorer.com/v1/staking/unstake_requests', () =>
410
- HttpResponse.json([
411
- {
412
- id: 77,
413
- cycle: 100,
414
- baker: { address },
415
- staker: { address },
416
- firstTime: '2026-05-01T00:00:00Z',
417
- status: 'pending',
418
- actualAmount: 10,
419
- },
420
- ])
421
- )
422
- )
388
+
389
+ mockedGetAccountByAddress.mockResolvedValue({
390
+ type: 'delegate',
391
+ balance: 100,
392
+ stakedBalance: 30,
393
+ unstakedBalance: 10,
394
+ })
395
+ mockedGetTokensBalances.mockResolvedValue([])
396
+ mockedGetUnstakeRequests.mockResolvedValue([
397
+ {
398
+ id: 77,
399
+ cycle: 100,
400
+ baker: { address },
401
+ staker: { address },
402
+ firstTime: '2026-05-01T00:00:00Z',
403
+ status: 'pending',
404
+ actualAmount: 10,
405
+ },
406
+ ])
423
407
 
424
408
  expect(await getBalance(context, address)).toEqual([
425
409
  { value: 100n, asset: { type: 'native' }, locked: 40n },
@@ -461,20 +445,14 @@ describe('getBalance', () => {
461
445
  })
462
446
 
463
447
  it('still excludes unstaked-frozen funds from spendable when the unstake_requests endpoint fails', async () => {
464
- mockServer.use(
465
- http.get(`http://tezos.explorer.com/v1/accounts/${address}`, () =>
466
- HttpResponse.json({
467
- type: 'user',
468
- balance: 100,
469
- unstakedBalance: 10,
470
- delegate: { address: delegateAddress },
471
- })
472
- ),
473
- http.get('http://tezos.explorer.com/v1/staking/unstake_requests', () =>
474
- HttpResponse.json({ error: 'internal' }, { status: 500 })
475
- ),
476
- http.get('http://tezos.explorer.com/v1/tokens/balances', () => HttpResponse.json([]))
477
- )
448
+ mockedGetAccountByAddress.mockResolvedValue({
449
+ type: 'user',
450
+ balance: 100,
451
+ unstakedBalance: 10,
452
+ delegate: { address: delegateAddress },
453
+ })
454
+ mockedGetUnstakeRequests.mockRejectedValue(new Error('endpoint failure'))
455
+ mockedGetTokensBalances.mockResolvedValue([])
478
456
 
479
457
  const result = await getBalance(context, address)
480
458