@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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { TezosContext } from '../config'
|
|
5
5
|
import type { getTezosToolkit } from './tezosToolkit'
|
|
6
|
-
import { log } from '@ledgerhq/logs'
|
|
7
6
|
import { getRevealFee, getRevealGasLimit } from '@taquito/taquito'
|
|
8
7
|
|
|
9
8
|
type RevealLimits = { fee: number; gasLimit: number; storageLimit: number }
|
|
@@ -14,19 +13,24 @@ type RevealLimits = { fee: number; gasLimit: number; storageLimit: number }
|
|
|
14
13
|
* (some addresses error with "inconsistent_hash").
|
|
15
14
|
*/
|
|
16
15
|
export async function estimateRevealLimits(
|
|
16
|
+
context: TezosContext,
|
|
17
17
|
tezosToolkit: ReturnType<typeof getTezosToolkit>,
|
|
18
|
-
address: string
|
|
19
|
-
feesConfig: TezosConfig['fees']
|
|
18
|
+
address: string
|
|
20
19
|
): Promise<RevealLimits> {
|
|
20
|
+
const { fees: feesConfig } = await context.config()
|
|
21
21
|
type RevealEstimate = { gasLimit?: number; storageLimit?: number; suggestedFeeMutez?: number }
|
|
22
22
|
let revealEstimate: RevealEstimate | undefined
|
|
23
23
|
try {
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
25
25
|
revealEstimate = (await tezosToolkit.estimate.reveal()) as RevealEstimate | undefined
|
|
26
26
|
} catch (error) {
|
|
27
|
-
|
|
28
|
-
error,
|
|
29
|
-
|
|
27
|
+
context.logger(
|
|
28
|
+
'estimate-error',
|
|
29
|
+
'error estimating reveal fees, trying to use getRevealGasLimit',
|
|
30
|
+
{
|
|
31
|
+
error,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
30
34
|
revealEstimate = {
|
|
31
35
|
gasLimit: getRevealGasLimit(address),
|
|
32
36
|
suggestedFeeMutez: getRevealFee(address),
|
package/src/logic/getBalance.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import type { TezosContext } from '../config'
|
|
5
5
|
import type { Balance } from '@ledgerhq/coin-module-framework/api/index'
|
|
6
|
-
import { log } from '@ledgerhq/logs'
|
|
7
6
|
import { hasManagerKey } from '../network/types'
|
|
8
7
|
import { createTzktApi } from '../network/tzkt'
|
|
9
8
|
import { partitionNativeBalance } from '../utils'
|
|
@@ -60,14 +59,18 @@ export async function getBalance(context: TezosContext, address: string): Promis
|
|
|
60
59
|
const normalized = BigInt(apiAccount.balance)
|
|
61
60
|
|
|
62
61
|
const unstakeRequests = await fetchUnstakeRequests(config, address, apiAccount).catch((error) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
context.logger(
|
|
63
|
+
'coin:tezos',
|
|
64
|
+
'getBalance: fetchUnstakeRequests failed; degrading stakes to []',
|
|
65
|
+
{
|
|
66
|
+
error,
|
|
67
|
+
address,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
67
70
|
return []
|
|
68
71
|
})
|
|
69
72
|
|
|
70
|
-
const stakes = buildStakesForAccount(address, apiAccount, unstakeRequests)
|
|
73
|
+
const stakes = buildStakesForAccount(context.logger, address, apiAccount, unstakeRequests)
|
|
71
74
|
const stakedBalance = BigInt(apiAccount.stakedBalance ?? 0)
|
|
72
75
|
const unstakedBalance = BigInt(apiAccount.unstakedBalance ?? 0)
|
|
73
76
|
const { locked } = partitionNativeBalance(normalized, stakedBalance, unstakedBalance)
|
package/src/logic/getBlock.ts
CHANGED
|
@@ -534,12 +534,12 @@ export async function getBlock(context: TezosContext, height: number): Promise<B
|
|
|
534
534
|
] = await Promise.all([
|
|
535
535
|
tzkt.getBlockByLevel(height),
|
|
536
536
|
tzkt.getBlockByLevel(height - 1),
|
|
537
|
-
fetchBlockTransactions(
|
|
538
|
-
fetchBlockTokenTransfers(
|
|
539
|
-
fetchBlockDelegations(
|
|
540
|
-
fetchBlockStaking(
|
|
541
|
-
fetchBlockOriginations(
|
|
542
|
-
fetchBlockReveals(
|
|
537
|
+
fetchBlockTransactions(context, height),
|
|
538
|
+
fetchBlockTokenTransfers(context, height),
|
|
539
|
+
fetchBlockDelegations(context, height),
|
|
540
|
+
fetchBlockStaking(context, height),
|
|
541
|
+
fetchBlockOriginations(context, height),
|
|
542
|
+
fetchBlockReveals(context, height),
|
|
543
543
|
])
|
|
544
544
|
|
|
545
545
|
// Token transfers triggered by originations from other blocks carry an
|
package/src/logic/getStakes.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { TezosCoinConfig, TezosContext } from '../config'
|
|
5
5
|
import type { APIAccount, APIManagerAccount, APIUnstakeRequest } from '../network/types'
|
|
6
6
|
import type { Cursor, Page, Stake } from '@ledgerhq/coin-module-framework/api/types'
|
|
7
|
-
import {
|
|
7
|
+
import type { Logger } from '@ledgerhq/coin-module-framework/config'
|
|
8
8
|
import { hasManagerKey } from '../network/types'
|
|
9
9
|
import { createTzktApi } from '../network/tzkt'
|
|
10
10
|
import { partitionNativeBalance } from '../utils'
|
|
@@ -28,17 +28,21 @@ export function fetchUnstakeRequests(
|
|
|
28
28
|
: Promise.resolve([])
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function unstakeRequestToStake(
|
|
31
|
+
function unstakeRequestToStake(
|
|
32
|
+
logger: Logger,
|
|
33
|
+
address: string,
|
|
34
|
+
req: APIUnstakeRequest
|
|
35
|
+
): Stake | null {
|
|
32
36
|
// TzKT's status filter is unreliable; never build a position for a finalized request.
|
|
33
37
|
if (req.status !== 'pending' && req.status !== 'finalizable') {
|
|
34
|
-
|
|
38
|
+
logger('coin:tezos', 'unstakeRequestToStake: dropping non-active unstake request', {
|
|
35
39
|
requestId: req.id,
|
|
36
40
|
status: req.status,
|
|
37
41
|
})
|
|
38
42
|
return null
|
|
39
43
|
}
|
|
40
44
|
if (req.actualAmount <= 0) {
|
|
41
|
-
|
|
45
|
+
logger('coin:tezos', 'unstakeRequestToStake: dropping non-positive unstake request', {
|
|
42
46
|
requestId: req.id,
|
|
43
47
|
actualAmount: req.actualAmount,
|
|
44
48
|
})
|
|
@@ -46,7 +50,7 @@ function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake |
|
|
|
46
50
|
}
|
|
47
51
|
const createdAt = new Date(req.firstTime)
|
|
48
52
|
if (!Number.isFinite(createdAt.getTime())) {
|
|
49
|
-
|
|
53
|
+
logger('coin:tezos', 'unstakeRequestToStake: dropping unstake request with invalid firstTime', {
|
|
50
54
|
requestId: req.id,
|
|
51
55
|
firstTime: req.firstTime,
|
|
52
56
|
})
|
|
@@ -67,6 +71,7 @@ function unstakeRequestToStake(address: string, req: APIUnstakeRequest): Stake |
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
export function buildStakesForAccount(
|
|
74
|
+
logger: Logger,
|
|
70
75
|
address: string,
|
|
71
76
|
account: APIManagerAccount,
|
|
72
77
|
unstakeRequests: APIUnstakeRequest[]
|
|
@@ -80,7 +85,7 @@ export function buildStakesForAccount(
|
|
|
80
85
|
|
|
81
86
|
const unstakeStakes: Stake[] = []
|
|
82
87
|
for (const req of unstakeRequests) {
|
|
83
|
-
const stake = unstakeRequestToStake(address, req)
|
|
88
|
+
const stake = unstakeRequestToStake(logger, address, req)
|
|
84
89
|
if (stake) unstakeStakes.push(stake)
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -93,7 +98,7 @@ export function buildStakesForAccount(
|
|
|
93
98
|
unstakedBalance
|
|
94
99
|
)
|
|
95
100
|
if (locked < stakedBalance + unstakedBalance) {
|
|
96
|
-
|
|
101
|
+
logger('coin:tezos', 'buildStakesForAccount: balance < staked + unstaked, clamping to 0', {
|
|
97
102
|
address,
|
|
98
103
|
balance: balance.toString(),
|
|
99
104
|
stakedBalance: stakedBalance.toString(),
|
|
@@ -137,5 +142,5 @@ export async function getStakes(
|
|
|
137
142
|
const accountInfo = await createTzktApi(config).getAccountByAddress(address)
|
|
138
143
|
if (!hasManagerKey(accountInfo)) return { items: [] }
|
|
139
144
|
const unstakeRequests = await fetchUnstakeRequests(config, address, accountInfo)
|
|
140
|
-
return { items: buildStakesForAccount(address, accountInfo, unstakeRequests) }
|
|
145
|
+
return { items: buildStakesForAccount(context.logger, address, accountInfo, unstakeRequests) }
|
|
141
146
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { TezosContext } from '../config'
|
|
5
5
|
import type { AccountsGetOperationsOptions } from '../network/types'
|
|
6
6
|
import type {
|
|
7
7
|
APIDelegationType,
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
TokenTransfersGetOptions,
|
|
15
15
|
} from '../network/types'
|
|
16
16
|
import type { Operation } from '@ledgerhq/coin-module-framework/api/types'
|
|
17
|
-
import {
|
|
17
|
+
import type { Logger } from '@ledgerhq/coin-module-framework/config'
|
|
18
18
|
import { STAKING_ACTION_TO_OP_TYPE } from '../constants'
|
|
19
19
|
import { createTzktApi } from '../network'
|
|
20
20
|
import {
|
|
@@ -69,6 +69,7 @@ function maxLevel<T extends { level: number }>(items: T[]): number | undefined {
|
|
|
69
69
|
|
|
70
70
|
/** When the API returned a full page, the trailing block level may be incomplete — drop it. */
|
|
71
71
|
function trimPartialLastLevel<T extends { level: number; id: number }>(
|
|
72
|
+
logger: Logger,
|
|
72
73
|
items: T[],
|
|
73
74
|
full: boolean
|
|
74
75
|
): { trimmed: T[]; intraLevelLastId?: number } {
|
|
@@ -78,7 +79,7 @@ function trimPartialLastLevel<T extends { level: number; id: number }>(
|
|
|
78
79
|
const lastLevel = last.level
|
|
79
80
|
const filtered = items.filter((op) => op.level !== lastLevel)
|
|
80
81
|
if (filtered.length === 0) {
|
|
81
|
-
|
|
82
|
+
logger('coin:tezos', 'listOperations: full page single level — keeping all rows', {
|
|
82
83
|
lastLevel,
|
|
83
84
|
count: items.length,
|
|
84
85
|
})
|
|
@@ -251,6 +252,7 @@ function keepNativeOp(op: APIOperation, address: string): op is ConvertibleOpera
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
function convertNativeOps(
|
|
255
|
+
logger: Logger,
|
|
254
256
|
ops: APIOperation[],
|
|
255
257
|
address: string,
|
|
256
258
|
stakingBlockHashes: Map<number, string>
|
|
@@ -282,7 +284,7 @@ function convertNativeOps(
|
|
|
282
284
|
return ops
|
|
283
285
|
.filter((op): op is ConvertibleOperation => keepNativeOp(op, address))
|
|
284
286
|
.map((op) => {
|
|
285
|
-
const converted = convertOperation(address, op, stakingBlockHashes)
|
|
287
|
+
const converted = convertOperation(logger, address, op, stakingBlockHashes)
|
|
286
288
|
const extraFee = internalFeesByHash.get(converted.tx.hash)
|
|
287
289
|
if (extraFee) {
|
|
288
290
|
// Add internal sub-tx fees to the parent operation, then clear to avoid
|
|
@@ -368,8 +370,8 @@ export async function listOperations(
|
|
|
368
370
|
const nativePage = clampPage(nativeOpsRaw, limit)
|
|
369
371
|
const tokenPage = clampPage(tokenTransfersRaw, limit)
|
|
370
372
|
|
|
371
|
-
const nativeTrimmed = trimPartialLastLevel(nativePage.sliced, nativePage.full)
|
|
372
|
-
const tokenTrimmed = trimPartialLastLevel(tokenPage.sliced, tokenPage.full)
|
|
373
|
+
const nativeTrimmed = trimPartialLastLevel(context.logger, nativePage.sliced, nativePage.full)
|
|
374
|
+
const tokenTrimmed = trimPartialLastLevel(context.logger, tokenPage.sliced, tokenPage.full)
|
|
373
375
|
|
|
374
376
|
// when both streams are exhausted (neither hit the page limit),
|
|
375
377
|
// we already have every op — alignment would silently discard ops
|
|
@@ -388,7 +390,7 @@ export async function listOperations(
|
|
|
388
390
|
? tokenTrimmed.trimmed
|
|
389
391
|
: alignToBoundary(tokenTrimmed.trimmed, boundary, sort)
|
|
390
392
|
|
|
391
|
-
const stakingBlockHashes = await fetchMissingStakingBlockHashes(
|
|
393
|
+
const stakingBlockHashes = await fetchMissingStakingBlockHashes(context, nativeAligned)
|
|
392
394
|
|
|
393
395
|
const parentMap = buildParentMap(nativeAligned)
|
|
394
396
|
|
|
@@ -401,7 +403,12 @@ export async function listOperations(
|
|
|
401
403
|
tokenTrimmed.intraLevelLastId
|
|
402
404
|
)
|
|
403
405
|
|
|
404
|
-
const filteredNativeOps = convertNativeOps(
|
|
406
|
+
const filteredNativeOps = convertNativeOps(
|
|
407
|
+
context.logger,
|
|
408
|
+
nativeAligned,
|
|
409
|
+
address,
|
|
410
|
+
stakingBlockHashes
|
|
411
|
+
)
|
|
405
412
|
const tokenConverted = convertTokenOps(tokenAligned, parentMap, address)
|
|
406
413
|
|
|
407
414
|
const sortedOperations = [...filteredNativeOps, ...tokenConverted].sort(
|
|
@@ -424,7 +431,7 @@ type ConvertibleOperation =
|
|
|
424
431
|
* failures are swallowed: callers fall back to `""` for levels not in the map.
|
|
425
432
|
*/
|
|
426
433
|
async function fetchMissingStakingBlockHashes(
|
|
427
|
-
|
|
434
|
+
context: TezosContext,
|
|
428
435
|
ops: readonly APIOperation[]
|
|
429
436
|
): Promise<Map<number, string>> {
|
|
430
437
|
const missingLevels = new Set<number>()
|
|
@@ -432,10 +439,11 @@ async function fetchMissingStakingBlockHashes(
|
|
|
432
439
|
if (isAPIStakingType(op) && !op.block) missingLevels.add(op.level)
|
|
433
440
|
}
|
|
434
441
|
if (missingLevels.size === 0) return new Map()
|
|
442
|
+
const config = await context.config()
|
|
435
443
|
try {
|
|
436
444
|
return await createTzktApi(config).getBlockHashesByLevels([...missingLevels])
|
|
437
445
|
} catch (err) {
|
|
438
|
-
|
|
446
|
+
context.logger('coin:tezos', 'fetchMissingStakingBlockHashes: skipped on fetch error', {
|
|
439
447
|
reason: String(err),
|
|
440
448
|
})
|
|
441
449
|
return new Map()
|
|
@@ -487,6 +495,7 @@ function resolveAmount(operation: ConvertibleOperation): bigint {
|
|
|
487
495
|
}
|
|
488
496
|
|
|
489
497
|
function resolveNormalizedType(
|
|
498
|
+
logger: Logger,
|
|
490
499
|
operation: ConvertibleOperation,
|
|
491
500
|
address: string,
|
|
492
501
|
targetAddress: string | undefined,
|
|
@@ -499,7 +508,7 @@ function resolveNormalizedType(
|
|
|
499
508
|
if (isAPIRevealType(operation)) return 'REVEAL'
|
|
500
509
|
if (isAPIOriginationType(operation)) return operation.contractBalance > 0 ? 'OUT' : 'FEES'
|
|
501
510
|
if (!isAPITransactionType(operation)) {
|
|
502
|
-
|
|
511
|
+
logger('coin:tezos', '(logic/operations): Unknown operation type, defaulting to OUT')
|
|
503
512
|
return 'OUT'
|
|
504
513
|
}
|
|
505
514
|
const isOut = operation.sender?.address === address
|
|
@@ -529,6 +538,7 @@ function getLedgerOpType(
|
|
|
529
538
|
}
|
|
530
539
|
|
|
531
540
|
function convertOperation(
|
|
541
|
+
logger: Logger,
|
|
532
542
|
address: string,
|
|
533
543
|
operation: ConvertibleOperation,
|
|
534
544
|
stakingBlockHashes: Map<number, string>
|
|
@@ -544,7 +554,7 @@ function convertOperation(
|
|
|
544
554
|
const targetAddress = resolveTargetAddress(operation)
|
|
545
555
|
// reveal has no meaningful target; staking resolves senders/recipients separately.
|
|
546
556
|
if (!targetAddress && !isAPIRevealType(operation) && !isAPIStakingType(operation)) {
|
|
547
|
-
|
|
557
|
+
logger('coin:tezos', '(logic/operations): No target address found for operation', operation)
|
|
548
558
|
}
|
|
549
559
|
|
|
550
560
|
const { senders, recipients } = isAPIStakingType(operation)
|
|
@@ -561,7 +571,7 @@ function convertOperation(
|
|
|
561
571
|
BigInt(operation.bakerFee ?? 0) +
|
|
562
572
|
BigInt(operation.allocationFee ?? 0)
|
|
563
573
|
|
|
564
|
-
const normalizedType = resolveNormalizedType(operation, address, targetAddress, amount)
|
|
574
|
+
const normalizedType = resolveNormalizedType(logger, operation, address, targetAddress, amount)
|
|
565
575
|
|
|
566
576
|
// Tezos uses "applied" for every success operation (something else=failed )
|
|
567
577
|
const hasFailed = operation.status && operation.status !== 'applied'
|
|
@@ -8,15 +8,17 @@ import {
|
|
|
8
8
|
InvalidAddressBecauseDestinationIsAlsoSource,
|
|
9
9
|
AmountRequired,
|
|
10
10
|
NotEnoughBalance,
|
|
11
|
+
UnexpectedGetBalanceError,
|
|
11
12
|
} from '@ledgerhq/coin-module-framework/errors'
|
|
12
13
|
import {
|
|
13
14
|
InvalidAddressBecauseAlreadyDelegated,
|
|
14
15
|
MustDelegateBeforeStaking,
|
|
15
16
|
NotEnoughBalanceToDelegate,
|
|
16
17
|
TezosNotEnoughStaked,
|
|
18
|
+
TezosStakeAmountTooSmall,
|
|
17
19
|
TezosStakeBlockedByPendingUnstake,
|
|
18
20
|
} from '../types/errors'
|
|
19
|
-
import { STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
|
|
21
|
+
import { MAPPED_TAQUITO_ERRORS, STAKE_USE_ALL_RESERVE_MUTEZ } from '../utils'
|
|
20
22
|
import { validateIntent } from './validateIntent'
|
|
21
23
|
|
|
22
24
|
const mockEstimateFees = jest.fn()
|
|
@@ -386,6 +388,35 @@ describe('validateIntent', () => {
|
|
|
386
388
|
expect(result.amount).toBe(0n)
|
|
387
389
|
})
|
|
388
390
|
|
|
391
|
+
it('should return TezosStakeAmountTooSmall when stake useAllAmount resolves max below the probe', async () => {
|
|
392
|
+
mockGetAccountByAddress.mockResolvedValue(
|
|
393
|
+
makeUserAccount({
|
|
394
|
+
delegate: { alias: 'baker', address: validRecipient, active: true },
|
|
395
|
+
delegationLevel: 1,
|
|
396
|
+
})
|
|
397
|
+
)
|
|
398
|
+
mockEstimateFees.mockResolvedValueOnce({
|
|
399
|
+
fees: 1000n,
|
|
400
|
+
gasLimit: 10000n,
|
|
401
|
+
storageLimit: 0n,
|
|
402
|
+
estimatedFees: 1000n,
|
|
403
|
+
amount: 999n,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const result = await validateIntent(context, {
|
|
407
|
+
intentType: 'staking',
|
|
408
|
+
asset: { type: 'native' },
|
|
409
|
+
type: 'stake',
|
|
410
|
+
sender: senderAddress,
|
|
411
|
+
recipient: '',
|
|
412
|
+
amount: 0n,
|
|
413
|
+
useAllAmount: true,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
expect(result.errors.amount).toBeInstanceOf(TezosStakeAmountTooSmall)
|
|
417
|
+
expect(result.amount).toBe(999n)
|
|
418
|
+
})
|
|
419
|
+
|
|
389
420
|
it('excludes already-staked funds when stake useAllAmount falls back to balance computation', async () => {
|
|
390
421
|
mockGetAccountByAddress.mockResolvedValue(
|
|
391
422
|
makeUserAccount({
|
|
@@ -986,6 +1017,65 @@ describe('validateIntent', () => {
|
|
|
986
1017
|
expect(result.errors.amount).toBeInstanceOf(MustDelegateBeforeStaking)
|
|
987
1018
|
})
|
|
988
1019
|
|
|
1020
|
+
it('maps stake_amount_too_small to TezosStakeAmountTooSmall', async () => {
|
|
1021
|
+
mockGetAccountByAddress.mockResolvedValue(
|
|
1022
|
+
makeUserAccount({
|
|
1023
|
+
delegate: { alias: 'baker', address: validRecipient, active: true },
|
|
1024
|
+
delegationLevel: 1,
|
|
1025
|
+
})
|
|
1026
|
+
)
|
|
1027
|
+
mockEstimateFees.mockResolvedValue({
|
|
1028
|
+
fees: 0n,
|
|
1029
|
+
gasLimit: 0n,
|
|
1030
|
+
storageLimit: 0n,
|
|
1031
|
+
estimatedFees: 500n,
|
|
1032
|
+
taquitoError: 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small',
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
const result = await validateIntent(context, {
|
|
1036
|
+
intentType: 'staking',
|
|
1037
|
+
asset: { type: 'native' },
|
|
1038
|
+
type: 'stake',
|
|
1039
|
+
sender: senderAddress,
|
|
1040
|
+
recipient: '',
|
|
1041
|
+
amount: 1n,
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
expect(result.errors.amount).toBeInstanceOf(TezosStakeAmountTooSmall)
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
it.each(MAPPED_TAQUITO_ERRORS)(
|
|
1048
|
+
'maps %s, which estimation reports instead of throwing, to a dedicated error',
|
|
1049
|
+
async (id) => {
|
|
1050
|
+
mockGetAccountByAddress.mockResolvedValue(
|
|
1051
|
+
makeUserAccount({
|
|
1052
|
+
delegate: { alias: 'baker', address: validRecipient, active: true },
|
|
1053
|
+
delegationLevel: 1,
|
|
1054
|
+
})
|
|
1055
|
+
)
|
|
1056
|
+
mockEstimateFees.mockResolvedValue({
|
|
1057
|
+
fees: 0n,
|
|
1058
|
+
gasLimit: 0n,
|
|
1059
|
+
storageLimit: 0n,
|
|
1060
|
+
estimatedFees: 500n,
|
|
1061
|
+
taquitoError: `proto.025-PsUshuai.${id}`,
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
const result = await validateIntent(context, {
|
|
1065
|
+
intentType: 'staking',
|
|
1066
|
+
asset: { type: 'native' },
|
|
1067
|
+
type: 'stake',
|
|
1068
|
+
sender: senderAddress,
|
|
1069
|
+
recipient: '',
|
|
1070
|
+
amount: 1000n,
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
const error = result.errors.amount ?? result.errors.recipient
|
|
1074
|
+
expect(error).toBeInstanceOf(Error)
|
|
1075
|
+
expect(error?.name).not.toBe('Error')
|
|
1076
|
+
}
|
|
1077
|
+
)
|
|
1078
|
+
|
|
989
1079
|
it('maps cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate to TezosStakeBlockedByPendingUnstake for stake', async () => {
|
|
990
1080
|
mockGetAccountByAddress.mockResolvedValue(
|
|
991
1081
|
makeUserAccount({
|
|
@@ -1163,7 +1253,7 @@ describe('validateIntent', () => {
|
|
|
1163
1253
|
expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
|
|
1164
1254
|
})
|
|
1165
1255
|
|
|
1166
|
-
it('should set amount to 0n when useAllAmount and token balance row is missing', async () => {
|
|
1256
|
+
it('should set amount to 0n and NotEnoughBalance when useAllAmount and token balance row is missing [LIVE-36768]', async () => {
|
|
1167
1257
|
const contract = 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU'
|
|
1168
1258
|
mockGetTokensBalances.mockResolvedValue([])
|
|
1169
1259
|
|
|
@@ -1179,6 +1269,113 @@ describe('validateIntent', () => {
|
|
|
1179
1269
|
|
|
1180
1270
|
expect(result.amount).toBe(0n)
|
|
1181
1271
|
expect(result.totalSpent).toBe(1000n)
|
|
1272
|
+
expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
it('send-max on a 0-balance token account blocks with NotEnoughBalance even when parent XTZ account has funds for fees [LIVE-36768]', async () => {
|
|
1276
|
+
const contract = 'KT1XnTn74bagnxYoFErbDH5QRAULUDtpXkmV'
|
|
1277
|
+
|
|
1278
|
+
mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ balance: 5_000_000 }))
|
|
1279
|
+
mockGetTokensBalances.mockResolvedValue([
|
|
1280
|
+
{
|
|
1281
|
+
id: 2,
|
|
1282
|
+
account: { address: senderAddress },
|
|
1283
|
+
token: {
|
|
1284
|
+
id: 2,
|
|
1285
|
+
contract: { address: contract },
|
|
1286
|
+
tokenId: '0',
|
|
1287
|
+
standard: 'fa2' as const,
|
|
1288
|
+
metadata: { symbol: 'USDt', decimals: '6' },
|
|
1289
|
+
},
|
|
1290
|
+
balance: '0',
|
|
1291
|
+
transfersCount: 0,
|
|
1292
|
+
firstLevel: 0,
|
|
1293
|
+
firstTime: '',
|
|
1294
|
+
lastLevel: 0,
|
|
1295
|
+
lastTime: '',
|
|
1296
|
+
},
|
|
1297
|
+
])
|
|
1298
|
+
|
|
1299
|
+
const result = await validateIntent(context, {
|
|
1300
|
+
intentType: 'transaction',
|
|
1301
|
+
asset: { type: 'token', assetReference: `${contract}:0` },
|
|
1302
|
+
type: 'send',
|
|
1303
|
+
sender: senderAddress,
|
|
1304
|
+
recipient: validRecipient,
|
|
1305
|
+
amount: 0n,
|
|
1306
|
+
useAllAmount: true,
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
expect(result.amount).toBe(0n)
|
|
1310
|
+
expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
it('send-max on a token account with a positive balance still resolves with no error [LIVE-36768 regression guard]', async () => {
|
|
1314
|
+
const contract = 'KT1XnTn74bagnxYoFErbDH5QRAULUDtpXkmV'
|
|
1315
|
+
const tokenBalance = '10000'
|
|
1316
|
+
|
|
1317
|
+
mockGetTokensBalances.mockResolvedValue([
|
|
1318
|
+
{
|
|
1319
|
+
id: 2,
|
|
1320
|
+
account: { address: senderAddress },
|
|
1321
|
+
token: {
|
|
1322
|
+
id: 2,
|
|
1323
|
+
contract: { address: contract },
|
|
1324
|
+
tokenId: '0',
|
|
1325
|
+
standard: 'fa2' as const,
|
|
1326
|
+
metadata: { symbol: 'USDt', decimals: '6' },
|
|
1327
|
+
},
|
|
1328
|
+
balance: tokenBalance,
|
|
1329
|
+
transfersCount: 0,
|
|
1330
|
+
firstLevel: 0,
|
|
1331
|
+
firstTime: '',
|
|
1332
|
+
lastLevel: 0,
|
|
1333
|
+
lastTime: '',
|
|
1334
|
+
},
|
|
1335
|
+
])
|
|
1336
|
+
|
|
1337
|
+
const result = await validateIntent(context, {
|
|
1338
|
+
intentType: 'transaction',
|
|
1339
|
+
asset: { type: 'token', assetReference: `${contract}:0` },
|
|
1340
|
+
type: 'send',
|
|
1341
|
+
sender: senderAddress,
|
|
1342
|
+
recipient: validRecipient,
|
|
1343
|
+
amount: 0n,
|
|
1344
|
+
useAllAmount: true,
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
expect(result.errors.amount).toBeUndefined()
|
|
1348
|
+
expect(result.amount).toBe(BigInt(tokenBalance))
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
it('send-max token: an unreachable TzKT blocks instead of returning the XTZ balance as a token amount [LIVE-36768]', async () => {
|
|
1352
|
+
// The wrong-unit fallback: with no token balance, calculateAmounts returned
|
|
1353
|
+
// spendable-XTZ-minus-fees (4_999_000 mutez) as the token amount, so the user could have
|
|
1354
|
+
// signed a 4.999 USDt transfer they never chose.
|
|
1355
|
+
const contract = 'KT1XnTn74bagnxYoFErbDH5QRAULUDtpXkmV'
|
|
1356
|
+
const cause = new Error('TzKT unreachable')
|
|
1357
|
+
|
|
1358
|
+
mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ balance: 5_000_000 }))
|
|
1359
|
+
mockGetTokensBalances.mockRejectedValue(cause)
|
|
1360
|
+
|
|
1361
|
+
const result = await validateIntent(context, {
|
|
1362
|
+
intentType: 'transaction',
|
|
1363
|
+
asset: { type: 'token', assetReference: `${contract}:0` },
|
|
1364
|
+
type: 'send',
|
|
1365
|
+
sender: senderAddress,
|
|
1366
|
+
recipient: validRecipient,
|
|
1367
|
+
amount: 0n,
|
|
1368
|
+
useAllAmount: true,
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
expect(result.amount).toBe(0n)
|
|
1372
|
+
expect(result.amount).not.toBe(4_999_000n)
|
|
1373
|
+
expect(result.estimatedFees).toBe(1000n) // fee estimation is preserved, not wiped
|
|
1374
|
+
// A holder of the token must not be told they have insufficient funds.
|
|
1375
|
+
const amountError: Error = result.errors.amount
|
|
1376
|
+
expect(amountError).toBeInstanceOf(UnexpectedGetBalanceError)
|
|
1377
|
+
expect(amountError).not.toBeInstanceOf(NotEnoughBalance)
|
|
1378
|
+
expect(amountError).toHaveProperty('cause', cause)
|
|
1182
1379
|
})
|
|
1183
1380
|
|
|
1184
1381
|
it('fixed-amount TKEY (18 dec): no error when user has enough token and XTZ for fees [LIVE-35976]', async () => {
|
|
@@ -1577,6 +1774,25 @@ describe('validateIntent', () => {
|
|
|
1577
1774
|
expect(result.amount).toBe(1_999_000n)
|
|
1578
1775
|
expect(result.totalSpent).toBe(2_000_000n)
|
|
1579
1776
|
})
|
|
1777
|
+
|
|
1778
|
+
it('blocks native send-max when spendable balance is exactly the fees [LIVE-36768]', async () => {
|
|
1779
|
+
// The native counterpart: totalSpent equals spendable exactly, so the
|
|
1780
|
+
// `totalSpent > spendable` coverage check passes and a 0-XTZ send used to go through.
|
|
1781
|
+
mockGetAccountByAddress.mockResolvedValue(makeUserAccount({ balance: 1000 })) // == estimatedFees
|
|
1782
|
+
|
|
1783
|
+
const result = await validateIntent(context, {
|
|
1784
|
+
intentType: 'transaction',
|
|
1785
|
+
asset: { type: 'native' },
|
|
1786
|
+
type: 'send',
|
|
1787
|
+
sender: senderAddress,
|
|
1788
|
+
recipient: validRecipient,
|
|
1789
|
+
amount: 0n,
|
|
1790
|
+
useAllAmount: true,
|
|
1791
|
+
})
|
|
1792
|
+
|
|
1793
|
+
expect(result.amount).toBe(0n)
|
|
1794
|
+
expect(result.errors.amount).toBeInstanceOf(NotEnoughBalance)
|
|
1795
|
+
})
|
|
1580
1796
|
})
|
|
1581
1797
|
|
|
1582
1798
|
describe('successful validation', () => {
|