@ledgerhq/coin-tezos 10.2.1 → 10.2.4
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.
- package/CHANGELOG.md +47 -0
- package/lib/api/index.d.ts +1 -3
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.js +4 -9
- package/lib/api/index.js.map +1 -1
- package/lib/logic/craftRawOperations.js +1 -1
- package/lib/logic/craftRawOperations.js.map +1 -1
- package/lib/logic/craftTransaction.d.ts.map +1 -1
- package/lib/logic/craftTransaction.js +1 -2
- package/lib/logic/craftTransaction.js.map +1 -1
- package/lib/logic/estimateFees.d.ts.map +1 -1
- package/lib/logic/estimateFees.js +51 -6
- package/lib/logic/estimateFees.js.map +1 -1
- package/lib/logic/estimateRevealLimits.d.ts +2 -2
- package/lib/logic/estimateRevealLimits.d.ts.map +1 -1
- package/lib/logic/estimateRevealLimits.js +3 -3
- package/lib/logic/estimateRevealLimits.js.map +1 -1
- package/lib/logic/getBalance.d.ts.map +1 -1
- package/lib/logic/getBalance.js +2 -3
- package/lib/logic/getBalance.js.map +1 -1
- package/lib/logic/getBlock.js +6 -6
- package/lib/logic/getBlock.js.map +1 -1
- package/lib/logic/getStakes.d.ts +2 -1
- package/lib/logic/getStakes.d.ts.map +1 -1
- package/lib/logic/getStakes.js +8 -9
- package/lib/logic/getStakes.js.map +1 -1
- package/lib/logic/listOperations.d.ts.map +1 -1
- package/lib/logic/listOperations.js +16 -16
- package/lib/logic/listOperations.js.map +1 -1
- package/lib/logic/validateIntent.d.ts.map +1 -1
- package/lib/logic/validateIntent.js +39 -9
- package/lib/logic/validateIntent.js.map +1 -1
- package/lib/network/bakers.d.ts +6 -5
- package/lib/network/bakers.d.ts.map +1 -1
- package/lib/network/bakers.js +13 -12
- package/lib/network/bakers.js.map +1 -1
- package/lib/network/tzkt.d.ts +8 -8
- package/lib/network/tzkt.d.ts.map +1 -1
- package/lib/network/tzkt.js +13 -11
- package/lib/network/tzkt.js.map +1 -1
- package/lib/types/errors.d.ts +3 -0
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/errors.js +8 -1
- package/lib/types/errors.js.map +1 -1
- package/lib/utils.d.ts +17 -0
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +31 -1
- package/lib/utils.js.map +1 -1
- package/lib-es/api/index.d.ts +1 -3
- package/lib-es/api/index.d.ts.map +1 -1
- package/lib-es/api/index.js +5 -10
- package/lib-es/api/index.js.map +1 -1
- package/lib-es/logic/craftRawOperations.js +1 -1
- package/lib-es/logic/craftRawOperations.js.map +1 -1
- package/lib-es/logic/craftTransaction.d.ts.map +1 -1
- package/lib-es/logic/craftTransaction.js +1 -2
- package/lib-es/logic/craftTransaction.js.map +1 -1
- package/lib-es/logic/estimateFees.d.ts.map +1 -1
- package/lib-es/logic/estimateFees.js +52 -7
- package/lib-es/logic/estimateFees.js.map +1 -1
- package/lib-es/logic/estimateRevealLimits.d.ts +2 -2
- package/lib-es/logic/estimateRevealLimits.d.ts.map +1 -1
- package/lib-es/logic/estimateRevealLimits.js +3 -3
- package/lib-es/logic/estimateRevealLimits.js.map +1 -1
- package/lib-es/logic/getBalance.d.ts.map +1 -1
- package/lib-es/logic/getBalance.js +2 -3
- package/lib-es/logic/getBalance.js.map +1 -1
- package/lib-es/logic/getBlock.js +6 -6
- package/lib-es/logic/getBlock.js.map +1 -1
- package/lib-es/logic/getStakes.d.ts +2 -1
- package/lib-es/logic/getStakes.d.ts.map +1 -1
- package/lib-es/logic/getStakes.js +8 -9
- package/lib-es/logic/getStakes.js.map +1 -1
- package/lib-es/logic/listOperations.d.ts.map +1 -1
- package/lib-es/logic/listOperations.js +16 -16
- package/lib-es/logic/listOperations.js.map +1 -1
- package/lib-es/logic/validateIntent.d.ts.map +1 -1
- package/lib-es/logic/validateIntent.js +42 -12
- package/lib-es/logic/validateIntent.js.map +1 -1
- package/lib-es/network/bakers.d.ts +6 -5
- package/lib-es/network/bakers.d.ts.map +1 -1
- package/lib-es/network/bakers.js +13 -12
- package/lib-es/network/bakers.js.map +1 -1
- package/lib-es/network/tzkt.d.ts +8 -8
- package/lib-es/network/tzkt.d.ts.map +1 -1
- package/lib-es/network/tzkt.js +13 -11
- package/lib-es/network/tzkt.js.map +1 -1
- package/lib-es/types/errors.d.ts +3 -0
- package/lib-es/types/errors.d.ts.map +1 -1
- package/lib-es/types/errors.js +6 -0
- package/lib-es/types/errors.js.map +1 -1
- package/lib-es/utils.d.ts +17 -0
- package/lib-es/utils.d.ts.map +1 -1
- package/lib-es/utils.js +29 -0
- package/lib-es/utils.js.map +1 -1
- package/package.json +3 -4
- package/src/api/index.test.ts +32 -0
- package/src/api/index.ts +5 -13
- package/src/logic/craftRawOperations.ts +1 -1
- package/src/logic/craftTransaction.ts +1 -2
- package/src/logic/estimateFees.test.ts +124 -2
- package/src/logic/estimateFees.ts +59 -6
- package/src/logic/estimateRevealLimits.ts +11 -7
- package/src/logic/getBalance.ts +9 -6
- package/src/logic/getBlock.ts +6 -6
- package/src/logic/getStakes.ts +13 -8
- package/src/logic/listOperations.ts +23 -13
- package/src/logic/validateIntent.test.ts +218 -2
- package/src/logic/validateIntent.ts +49 -9
- package/src/network/bakers.integ.test.ts +3 -2
- package/src/network/bakers.test.ts +2 -1
- package/src/network/bakers.ts +15 -13
- package/src/network/tzkt.test.ts +29 -30
- package/src/network/tzkt.ts +38 -23
- package/src/types/errors.ts +7 -0
- package/src/utils.ts +32 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NotEnoughBalance,
|
|
15
15
|
AmountRequired,
|
|
16
16
|
InvalidAddressBecauseDestinationIsAlsoSource,
|
|
17
|
+
UnexpectedGetBalanceError,
|
|
17
18
|
} from '@ledgerhq/coin-module-framework/errors'
|
|
18
19
|
import { validateAddress, ValidationResult } from '@taquito/utils'
|
|
19
20
|
import { hasManagerKey } from '../network/types'
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
MustDelegateBeforeStaking,
|
|
24
25
|
NotEnoughBalanceToDelegate,
|
|
25
26
|
TezosNotEnoughStaked,
|
|
27
|
+
TezosStakeAmountTooSmall,
|
|
26
28
|
TezosStakeBlockedByPendingUnstake,
|
|
27
29
|
} from '../types/errors'
|
|
28
30
|
import {
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
parseTezosTokenAsset,
|
|
31
33
|
partitionNativeBalance,
|
|
32
34
|
resolveTezosOperationMode,
|
|
35
|
+
STAKE_ESTIMATION_PROBE_MUTEZ,
|
|
33
36
|
} from '../utils'
|
|
34
37
|
import { estimateFees } from './estimateFees'
|
|
35
38
|
|
|
@@ -155,6 +158,9 @@ function mapTaquitoErrors(taquitoError: string, intentType: string): Record<stri
|
|
|
155
158
|
errors.amount = new TezosNotEnoughStaked()
|
|
156
159
|
} else if (taquitoError.endsWith('contract.must_be_delegated_to_stake')) {
|
|
157
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()
|
|
158
164
|
} else if (
|
|
159
165
|
taquitoError.endsWith('cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate')
|
|
160
166
|
) {
|
|
@@ -165,10 +171,10 @@ function mapTaquitoErrors(taquitoError: string, intentType: string): Record<stri
|
|
|
165
171
|
// Re-delegating (or staking) to the current baker leaves the delegate unchanged; the node
|
|
166
172
|
// rejects it. Surfaces for both `delegate` and `stake` intents as "already delegated".
|
|
167
173
|
errors.recipient = new InvalidAddressBecauseAlreadyDelegated()
|
|
168
|
-
} else if (taquitoError.
|
|
174
|
+
} else if (taquitoError.endsWith('empty_implicit_contract')) {
|
|
169
175
|
errors.amount =
|
|
170
176
|
intentType === 'stake' ? new NotEnoughBalance() : new NotEnoughBalanceToDelegate()
|
|
171
|
-
} else if (taquitoError.
|
|
177
|
+
} else if (taquitoError.endsWith('script_rejected')) {
|
|
172
178
|
errors.amount = new NotEnoughBalance()
|
|
173
179
|
} else {
|
|
174
180
|
errors.amount = new Error(taquitoError)
|
|
@@ -397,17 +403,25 @@ export async function validateIntent(
|
|
|
397
403
|
// the outer handler (which would wipe the already-computed estimatedFees and add a spurious
|
|
398
404
|
// errors.estimation on top of the real fee-estimation error).
|
|
399
405
|
let tokenBalance: bigint | undefined
|
|
406
|
+
let tokenBalanceError: unknown
|
|
400
407
|
if (errors.amount && !intent.useAllAmount) {
|
|
401
408
|
tokenBalance = undefined
|
|
402
409
|
} else {
|
|
403
410
|
try {
|
|
404
411
|
tokenBalance = await fetchTokenBalance(config, intent)
|
|
405
|
-
} catch {
|
|
406
|
-
tokenBalance = undefined
|
|
412
|
+
} catch (e) {
|
|
413
|
+
tokenBalance = undefined
|
|
414
|
+
tokenBalanceError = e
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
|
-
|
|
410
|
-
|
|
417
|
+
|
|
418
|
+
// Never pass `undefined` for a token send-max: that triggers the native-branch/wrong-unit
|
|
419
|
+
// fallback described above. An unresolved balance becomes 0n; the guards below set the error.
|
|
420
|
+
const isTokenSendMax =
|
|
421
|
+
intent.type === 'send' &&
|
|
422
|
+
!!intent.useAllAmount &&
|
|
423
|
+
resolveTezosOperationMode(intent.type, intent.asset) === 'send_token'
|
|
424
|
+
const tokenBalanceForSendMax = isTokenSendMax ? (tokenBalance ?? 0n) : undefined
|
|
411
425
|
|
|
412
426
|
const amounts = calculateAmounts(
|
|
413
427
|
intent,
|
|
@@ -419,10 +433,36 @@ export async function validateIntent(
|
|
|
419
433
|
amount = amounts.amount
|
|
420
434
|
totalSpent = amounts.totalSpent
|
|
421
435
|
|
|
422
|
-
|
|
436
|
+
// An unreachable TzKT is not an empty balance. Both block, but reporting NotEnoughBalance
|
|
437
|
+
// here would tell a user who holds the token that they have insufficient funds.
|
|
438
|
+
if (isTokenSendMax && tokenBalanceError !== undefined && !errors.amount) {
|
|
439
|
+
errors.amount = new UnexpectedGetBalanceError(undefined, tokenBalanceError)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// A send-max resolving to 0n must block. validateBalanceCoverage below only compares native
|
|
443
|
+
// spendable against totalSpent, which a 0-amount transaction always satisfies, so nothing
|
|
444
|
+
// else catches it.
|
|
445
|
+
// `finalize_unstake` is excluded: it is parameter-less and 0n by construction.
|
|
446
|
+
if (
|
|
447
|
+
(intent.type === 'send' || intent.type === 'stake') &&
|
|
448
|
+
intent.useAllAmount &&
|
|
449
|
+
amount === 0n &&
|
|
450
|
+
!errors.amount
|
|
451
|
+
) {
|
|
423
452
|
errors.amount = new NotEnoughBalance()
|
|
424
453
|
}
|
|
425
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
|
+
|
|
426
466
|
const { spendable } = partitionNativeBalance(
|
|
427
467
|
BigInt(senderInfo.balance),
|
|
428
468
|
BigInt(senderInfo.stakedBalance ?? 0),
|
|
@@ -431,8 +471,8 @@ export async function validateIntent(
|
|
|
431
471
|
const balanceErrors = validateBalanceCoverage(spendable, totalSpent)
|
|
432
472
|
Object.assign(errors, balanceErrors)
|
|
433
473
|
|
|
434
|
-
// Token balance coverage for fixed-amount token sends.
|
|
435
|
-
//
|
|
474
|
+
// Token balance coverage for fixed-amount token sends. send-max needs none: amount is the
|
|
475
|
+
// token balance itself, and the zero/unresolved cases already errored above.
|
|
436
476
|
if (
|
|
437
477
|
!errors.amount &&
|
|
438
478
|
intent.type === 'send' &&
|
|
@@ -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(
|
|
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
|
})
|
package/src/network/bakers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
76
|
-
const
|
|
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
|
-
|
|
90
|
+
context: TezosContext,
|
|
90
91
|
whitelistAddresses: string[]
|
|
91
92
|
): Promise<Baker[]> => {
|
|
92
|
-
|
|
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(
|
|
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
|
|
104
|
-
const
|
|
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 => {
|
package/src/network/tzkt.test.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 { 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
|
|
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(
|
|
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(
|
|
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(
|
|
842
|
+
const result = await fetchAllTransactions(context, 'tz1acc')
|
|
844
843
|
|
|
845
844
|
expect(result).toEqual([opMissingId])
|
|
846
|
-
expect(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1033
|
+
const result = await fetchBlockTransactions(context, 100)
|
|
1035
1034
|
|
|
1036
1035
|
expect(mockedNetwork).toHaveBeenCalledTimes(100)
|
|
1037
1036
|
expect(result).toHaveLength(100_000)
|
|
1038
|
-
expect(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1090
|
+
const result = await fetchBlockTokenTransfers(context, 100)
|
|
1092
1091
|
|
|
1093
1092
|
expect(mockedNetwork).toHaveBeenCalledTimes(100)
|
|
1094
1093
|
expect(result).toHaveLength(100_000)
|
|
1095
|
-
expect(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1186
|
+
const result = await fetchBlockDelegations(context, 100)
|
|
1188
1187
|
|
|
1189
1188
|
expect(mockedNetwork).toHaveBeenCalledTimes(100)
|
|
1190
1189
|
expect(result).toHaveLength(100_000)
|
|
1191
|
-
expect(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1282
|
+
const result = await fetchBlockStaking(context, 100)
|
|
1284
1283
|
|
|
1285
1284
|
expect(mockedNetwork).toHaveBeenCalledTimes(100)
|
|
1286
1285
|
expect(result).toHaveLength(100_000)
|
|
1287
|
-
expect(
|
|
1286
|
+
expect(mockLogger).toHaveBeenCalledWith(
|
|
1288
1287
|
'tezos',
|
|
1289
1288
|
expect.stringContaining('fetchBlockStaking: maxTxQuery limit reached at level 100')
|
|
1290
1289
|
)
|
package/src/network/tzkt.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 { TezosCoinConfig } from '../config'
|
|
4
|
+
import type { TezosCoinConfig, TezosContext } from '../config'
|
|
5
5
|
import type {
|
|
6
6
|
APIAccount,
|
|
7
7
|
APIBlock,
|
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
} from './types'
|
|
20
20
|
import URL from 'url'
|
|
21
21
|
import network from '@ledgerhq/live-network'
|
|
22
|
-
import { log } from '@ledgerhq/logs'
|
|
23
22
|
|
|
24
23
|
/** TzKT hard-caps `limit` at 10 000; we use a safer page size to stay well under that. */
|
|
25
24
|
const BLOCK_PAGE_SIZE = 1000
|
|
@@ -466,10 +465,11 @@ export type TzktApi = ReturnType<typeof createTzktApi>
|
|
|
466
465
|
|
|
467
466
|
// TODO this has same purpose as api/listOperations
|
|
468
467
|
export const fetchAllTransactions = async (
|
|
469
|
-
|
|
468
|
+
context: TezosContext,
|
|
470
469
|
address: string,
|
|
471
470
|
lastId?: number
|
|
472
471
|
): Promise<APIOperation[]> => {
|
|
472
|
+
const config = await context.config()
|
|
473
473
|
const api = createTzktApi(config)
|
|
474
474
|
let ops: APIOperation[] = []
|
|
475
475
|
let maxIteration = config.explorer.maxTxQuery
|
|
@@ -485,7 +485,7 @@ export const fetchAllTransactions = async (
|
|
|
485
485
|
if (!last) return ops
|
|
486
486
|
lastId = last.id
|
|
487
487
|
if (!lastId) {
|
|
488
|
-
|
|
488
|
+
context.logger('tezos', 'id missing!')
|
|
489
489
|
return ops
|
|
490
490
|
}
|
|
491
491
|
} while (--maxIteration)
|
|
@@ -500,11 +500,13 @@ export const fetchAllTransactions = async (
|
|
|
500
500
|
* A safety cap (`maxTxQuery`) prevents infinite loops on pathological responses.
|
|
501
501
|
*/
|
|
502
502
|
async function fetchBlockPaginated<T extends { id: number }>(
|
|
503
|
-
|
|
504
|
-
|
|
503
|
+
context: TezosContext,
|
|
504
|
+
pageFnFactory: (config: TezosCoinConfig) => (level: number, cursor?: number) => Promise<T[]>,
|
|
505
505
|
level: number,
|
|
506
506
|
label: string
|
|
507
507
|
): Promise<T[]> {
|
|
508
|
+
const config = await context.config()
|
|
509
|
+
const pageFn = pageFnFactory(config)
|
|
508
510
|
const items: T[] = []
|
|
509
511
|
let cursor: number | undefined
|
|
510
512
|
let maxIteration = config.explorer.maxTxQuery
|
|
@@ -516,45 +518,58 @@ async function fetchBlockPaginated<T extends { id: number }>(
|
|
|
516
518
|
cursor = page.at(-1)!.id
|
|
517
519
|
} while (--maxIteration > 0)
|
|
518
520
|
if (maxIteration === 0) {
|
|
519
|
-
|
|
521
|
+
context.logger(
|
|
522
|
+
'tezos',
|
|
523
|
+
`${label}: maxTxQuery limit reached at level ${level}, result may be incomplete`
|
|
524
|
+
)
|
|
520
525
|
}
|
|
521
526
|
return items
|
|
522
527
|
}
|
|
523
528
|
|
|
524
|
-
export const fetchBlockTransactions = (
|
|
529
|
+
export const fetchBlockTransactions = (context: TezosContext, level: number) =>
|
|
525
530
|
fetchBlockPaginated(
|
|
526
|
-
|
|
527
|
-
createTzktApi(config).getBlockTransactionsPage,
|
|
531
|
+
context,
|
|
532
|
+
(config) => createTzktApi(config).getBlockTransactionsPage,
|
|
528
533
|
level,
|
|
529
534
|
'fetchBlockTransactions'
|
|
530
535
|
)
|
|
531
536
|
|
|
532
|
-
export const fetchBlockTokenTransfers = (
|
|
537
|
+
export const fetchBlockTokenTransfers = (context: TezosContext, level: number) =>
|
|
533
538
|
fetchBlockPaginated(
|
|
534
|
-
|
|
535
|
-
createTzktApi(config).getBlockTokenTransfersPage,
|
|
539
|
+
context,
|
|
540
|
+
(config) => createTzktApi(config).getBlockTokenTransfersPage,
|
|
536
541
|
level,
|
|
537
542
|
'fetchBlockTokenTransfers'
|
|
538
543
|
)
|
|
539
544
|
|
|
540
|
-
export const fetchBlockDelegations = (
|
|
545
|
+
export const fetchBlockDelegations = (context: TezosContext, level: number) =>
|
|
541
546
|
fetchBlockPaginated(
|
|
542
|
-
|
|
543
|
-
createTzktApi(config).getBlockDelegationsPage,
|
|
547
|
+
context,
|
|
548
|
+
(config) => createTzktApi(config).getBlockDelegationsPage,
|
|
544
549
|
level,
|
|
545
550
|
'fetchBlockDelegations'
|
|
546
551
|
)
|
|
547
552
|
|
|
548
|
-
export const fetchBlockStaking = (
|
|
549
|
-
fetchBlockPaginated(
|
|
553
|
+
export const fetchBlockStaking = (context: TezosContext, level: number) =>
|
|
554
|
+
fetchBlockPaginated(
|
|
555
|
+
context,
|
|
556
|
+
(config) => createTzktApi(config).getBlockStakingPage,
|
|
557
|
+
level,
|
|
558
|
+
'fetchBlockStaking'
|
|
559
|
+
)
|
|
550
560
|
|
|
551
|
-
export const fetchBlockOriginations = (
|
|
561
|
+
export const fetchBlockOriginations = (context: TezosContext, level: number) =>
|
|
552
562
|
fetchBlockPaginated(
|
|
553
|
-
|
|
554
|
-
createTzktApi(config).getBlockOriginationsPage,
|
|
563
|
+
context,
|
|
564
|
+
(config) => createTzktApi(config).getBlockOriginationsPage,
|
|
555
565
|
level,
|
|
556
566
|
'fetchBlockOriginations'
|
|
557
567
|
)
|
|
558
568
|
|
|
559
|
-
export const fetchBlockReveals = (
|
|
560
|
-
fetchBlockPaginated(
|
|
569
|
+
export const fetchBlockReveals = (context: TezosContext, level: number) =>
|
|
570
|
+
fetchBlockPaginated(
|
|
571
|
+
context,
|
|
572
|
+
(config) => createTzktApi(config).getBlockRevealsPage,
|
|
573
|
+
level,
|
|
574
|
+
'fetchBlockReveals'
|
|
575
|
+
)
|
package/src/types/errors.ts
CHANGED
|
@@ -59,3 +59,10 @@ export class TezosStakeBlockedByPendingUnstake extends Error {
|
|
|
59
59
|
this.name = 'TezosStakeBlockedByPendingUnstake'
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
export class TezosStakeAmountTooSmall extends Error {
|
|
64
|
+
constructor() {
|
|
65
|
+
super('Stake amount too small to mint any staking pseudotoken')
|
|
66
|
+
this.name = 'TezosStakeAmountTooSmall'
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -20,6 +20,38 @@ export const DUST_MARGIN_MUTEZ = 500
|
|
|
20
20
|
// Tezos rejects `empty_implicit_delegated_contract`; stake-max leaves this buffer.
|
|
21
21
|
export const STAKE_USE_ALL_RESERVE_MUTEZ = 10_000n
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Amount pre-estimated for a stake whose real amount isn't known yet (stake-max), and the floor
|
|
25
|
+
* a resolved stake-max must clear: only the probe is simulated, never the resolved amount.
|
|
26
|
+
*
|
|
27
|
+
* Octez rejects a stake that mints no pseudotoken (`stake_amount_too_small`): issued
|
|
28
|
+
* pseudotokens are `floor(amount * pseudotokens / staked_tez)`, so 1 mutez fails for any
|
|
29
|
+
* delegate whose stakers accrued rewards — a 2 mutez floor across delegates in 2026-09.
|
|
30
|
+
* https://gitlab.com/tezos/tezos/-/commit/b8339f8893e1da45e73ea5c4b1b82a0dfefb503c
|
|
31
|
+
*/
|
|
32
|
+
export const STAKE_ESTIMATION_PROBE_MUTEZ = 1_000n
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Suffixes of the protocol error ids `validateIntent` turns into field errors. Estimation reports
|
|
36
|
+
* these as `taquitoError` and the api returns them, so the mapping gets its turn instead of the
|
|
37
|
+
* raw id reaching the user. Any other protocol error keeps propagating — callers retry on it.
|
|
38
|
+
*/
|
|
39
|
+
export const MAPPED_TAQUITO_ERRORS = [
|
|
40
|
+
'balance_too_low',
|
|
41
|
+
'cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate',
|
|
42
|
+
'contract.must_be_delegated_to_stake',
|
|
43
|
+
'delegate.unchanged',
|
|
44
|
+
'empty_implicit_contract',
|
|
45
|
+
'script_rejected',
|
|
46
|
+
'stake_amount_too_small',
|
|
47
|
+
'staking.too_much_unstaked',
|
|
48
|
+
'subtraction_underflow',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export function isMappedTaquitoError(id: string): boolean {
|
|
52
|
+
return MAPPED_TAQUITO_ERRORS.some((mapped) => id.endsWith(mapped))
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
/** Stake "Use Max": staked and unstaked-frozen funds count toward `balance` but aren't (re-)stakeable until finalized, so subtract both. */
|
|
24
56
|
export function computeMaxStakeAmount(
|
|
25
57
|
balance: bigint,
|