@ledgerhq/coin-tezos 10.2.2 → 10.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/lib/api/index.d.ts +1 -3
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.js +49 -26
- package/lib/api/index.js.map +1 -1
- package/lib/logic/craftRawOperations.js +3 -4
- package/lib/logic/craftRawOperations.js.map +1 -1
- package/lib/logic/craftTransaction.d.ts +6 -0
- package/lib/logic/craftTransaction.d.ts.map +1 -1
- package/lib/logic/craftTransaction.js +2 -3
- package/lib/logic/craftTransaction.js.map +1 -1
- package/lib/logic/estimateFees.d.ts +7 -1
- package/lib/logic/estimateFees.d.ts.map +1 -1
- package/lib/logic/estimateFees.js +81 -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 +14 -2
- 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 +50 -27
- package/lib-es/api/index.js.map +1 -1
- package/lib-es/logic/craftRawOperations.js +3 -4
- package/lib-es/logic/craftRawOperations.js.map +1 -1
- package/lib-es/logic/craftTransaction.d.ts +6 -0
- package/lib-es/logic/craftTransaction.d.ts.map +1 -1
- package/lib-es/logic/craftTransaction.js +2 -3
- package/lib-es/logic/craftTransaction.js.map +1 -1
- package/lib-es/logic/estimateFees.d.ts +7 -1
- package/lib-es/logic/estimateFees.d.ts.map +1 -1
- package/lib-es/logic/estimateFees.js +82 -8
- 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 +16 -4
- 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 +208 -0
- package/src/api/index.ts +55 -28
- package/src/logic/craftRawOperations.ts +3 -4
- package/src/logic/craftTransaction.test.ts +30 -0
- package/src/logic/craftTransaction.ts +8 -3
- package/src/logic/estimateFees.test.ts +224 -2
- package/src/logic/estimateFees.ts +92 -7
- 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 +90 -1
- package/src/logic/validateIntent.ts +18 -2
- 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
package/src/api/index.test.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
createSendTransactionIntentMockData,
|
|
9
9
|
createStakingTransactionIntentMockData,
|
|
10
10
|
} from '@ledgerhq/coin-module-framework/test-utils'
|
|
11
|
+
import { getRevealFee } from '@taquito/taquito'
|
|
12
|
+
import { MAPPED_TAQUITO_ERRORS } from '../utils'
|
|
11
13
|
import { createApi } from './index'
|
|
12
14
|
|
|
13
15
|
// The tzkt factory `createTzktApi(config)` builds a fresh object on every call, so a spy on a
|
|
@@ -298,6 +300,69 @@ describe('craftTransaction', () => {
|
|
|
298
300
|
)
|
|
299
301
|
})
|
|
300
302
|
|
|
303
|
+
it('splits the reported total so the crafted batch fees sum back to it', async () => {
|
|
304
|
+
// 300 no longer clamps both former reveal-fee sources to the same floor: the estimation
|
|
305
|
+
// reported Taquito's static constant while the reveal carried the node's live suggestion.
|
|
306
|
+
const minFees = 300
|
|
307
|
+
const unrevealedSender = 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr'
|
|
308
|
+
const txFee = 2000n
|
|
309
|
+
const staticRevealFee = BigInt(getRevealFee(unrevealedSender))
|
|
310
|
+
const reportedTotal = txFee + staticRevealFee
|
|
311
|
+
|
|
312
|
+
const contextLowMinFees = makeContext({
|
|
313
|
+
status: { type: 'active' },
|
|
314
|
+
baker: { url: 'https://baker.example.com' },
|
|
315
|
+
explorer: { url: 'foo', maxTxQuery: 1 },
|
|
316
|
+
node: { url: 'bar' },
|
|
317
|
+
fees: {
|
|
318
|
+
minGasLimit: 600,
|
|
319
|
+
minRevealGasLimit: 300,
|
|
320
|
+
minStorageLimit: 0,
|
|
321
|
+
minFees,
|
|
322
|
+
minEstimatedFees: minFees,
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const unrevealedAccount: APIAccount = {
|
|
327
|
+
type: 'user',
|
|
328
|
+
balance: 1000000,
|
|
329
|
+
revealed: false,
|
|
330
|
+
address: unrevealedSender,
|
|
331
|
+
publicKey: 'edpktest',
|
|
332
|
+
counter: 0,
|
|
333
|
+
delegationLevel: 0,
|
|
334
|
+
delegationTime: '2021-01-01T00:00:00Z',
|
|
335
|
+
numTransactions: 0,
|
|
336
|
+
firstActivityTime: '2021-01-01T00:00:00Z',
|
|
337
|
+
}
|
|
338
|
+
mockGetAccountByAddress
|
|
339
|
+
.mockResolvedValueOnce(unrevealedAccount)
|
|
340
|
+
.mockResolvedValueOnce(unrevealedAccount)
|
|
341
|
+
|
|
342
|
+
logicEstimateFees.mockResolvedValue({
|
|
343
|
+
estimatedFees: reportedTotal,
|
|
344
|
+
fees: txFee,
|
|
345
|
+
gasLimit: 10000n,
|
|
346
|
+
storageLimit: 0n,
|
|
347
|
+
parameters: { gasLimit: 10000n, storageLimit: 0n, txFee },
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
await api.craftTransaction(
|
|
351
|
+
contextLowMinFees,
|
|
352
|
+
createSendTransactionIntentMockData({
|
|
353
|
+
type: 'send',
|
|
354
|
+
sender: unrevealedSender,
|
|
355
|
+
senderPublicKey: '00d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
|
|
356
|
+
recipient: 'tz1recipient',
|
|
357
|
+
amount: 1000n,
|
|
358
|
+
})
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
const craftedFee = logicCraftTransactionMock.mock.calls[0][2].fee
|
|
362
|
+
expect(BigInt(craftedFee.fees) + BigInt(craftedFee.revealFees)).toBe(reportedTotal)
|
|
363
|
+
expect(craftedFee.revealFees).toBe(staticRevealFee.toString())
|
|
364
|
+
})
|
|
365
|
+
|
|
301
366
|
it('craft transaction with customFee splits total for unrevealed delegate so total equals customFee', async () => {
|
|
302
367
|
const minFees = 1000
|
|
303
368
|
const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
|
|
@@ -584,6 +649,118 @@ describe('estimateFees', () => {
|
|
|
584
649
|
expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT)
|
|
585
650
|
})
|
|
586
651
|
|
|
652
|
+
it('does not retry keyless for a token estimate after an unrelated failure', async () => {
|
|
653
|
+
// A keyless retry here would surface as PublicKeyNotFoundError and hide the real error.
|
|
654
|
+
const nodeError = Object.assign(new Error('Http error response: (500)'), {
|
|
655
|
+
status: 500,
|
|
656
|
+
body: '[{"kind":"permanent","id":"proto.025-PsUshuai.gas_limit_too_high"}]',
|
|
657
|
+
})
|
|
658
|
+
logicEstimateFees.mockRejectedValue(nodeError)
|
|
659
|
+
|
|
660
|
+
await expect(
|
|
661
|
+
api.estimateFees(context, {
|
|
662
|
+
intentType: 'transaction',
|
|
663
|
+
type: 'send',
|
|
664
|
+
sender: 'tz1test',
|
|
665
|
+
recipient: 'tz1recipient',
|
|
666
|
+
amount: 1000n,
|
|
667
|
+
asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' },
|
|
668
|
+
})
|
|
669
|
+
).rejects.toThrow('Http error response: (500)')
|
|
670
|
+
|
|
671
|
+
expect(logicEstimateFees).toHaveBeenCalledTimes(1)
|
|
672
|
+
logicEstimateFees.mockReset()
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('still retries keyless for a native estimate after an unrelated failure', async () => {
|
|
676
|
+
// Native estimation has no gas cap yet, so it hits the same block-gas overflow and depends
|
|
677
|
+
// on the keyless retry to reach the fallback. Without it an ordinary send reports no fees.
|
|
678
|
+
const blockOverflow = Object.assign(new Error('Http error response: (500)'), {
|
|
679
|
+
status: 500,
|
|
680
|
+
body: '[{"kind":"temporary","id":"proto.025-PsUshuai.gas_exhausted.block"}]',
|
|
681
|
+
})
|
|
682
|
+
logicEstimateFees
|
|
683
|
+
.mockRejectedValueOnce(blockOverflow)
|
|
684
|
+
.mockRejectedValueOnce(new Error('Public key not found'))
|
|
685
|
+
|
|
686
|
+
const unrevealedSender = 'tz1unrevealed'
|
|
687
|
+
const unrevealedAccount: APIAccount = {
|
|
688
|
+
type: 'user',
|
|
689
|
+
balance: 1000000,
|
|
690
|
+
revealed: false,
|
|
691
|
+
address: unrevealedSender,
|
|
692
|
+
publicKey: undefined,
|
|
693
|
+
counter: 0,
|
|
694
|
+
delegationLevel: 0,
|
|
695
|
+
delegationTime: '2021-01-01T00:00:00Z',
|
|
696
|
+
numTransactions: 0,
|
|
697
|
+
firstActivityTime: '2021-01-01T00:00:00Z',
|
|
698
|
+
}
|
|
699
|
+
mockGetAccountByAddress.mockResolvedValue(unrevealedAccount)
|
|
700
|
+
mockGetTezosToolkit.mockReturnValueOnce({
|
|
701
|
+
estimate: { transfer: jest.fn().mockRejectedValue(new Error('Public key not found')) },
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
const result = await api.estimateFees(
|
|
705
|
+
context,
|
|
706
|
+
createSendTransactionIntentMockData({
|
|
707
|
+
type: 'send',
|
|
708
|
+
sender: unrevealedSender,
|
|
709
|
+
recipient: 'tz1recipient',
|
|
710
|
+
amount: 1000n,
|
|
711
|
+
})
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
expect(logicEstimateFees).toHaveBeenCalledTimes(2)
|
|
715
|
+
expect(result.parameters?.gasLimit).toBe(2500n)
|
|
716
|
+
logicEstimateFees.mockReset()
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it('retries keyless for contract.manager.inconsistent_hash', async () => {
|
|
720
|
+
const inconsistentHash = Object.assign(new Error('Http error response: (500)'), {
|
|
721
|
+
status: 500,
|
|
722
|
+
body: '[{"kind":"permanent","id":"proto.025-PsUshuai.contract.manager.inconsistent_hash"}]',
|
|
723
|
+
})
|
|
724
|
+
logicEstimateFees.mockRejectedValueOnce(inconsistentHash).mockResolvedValueOnce({
|
|
725
|
+
estimatedFees: DEFAULT_ESTIMATED_FEES,
|
|
726
|
+
gasLimit: DEFAULT_GAS_LIMIT,
|
|
727
|
+
storageLimit: DEFAULT_STORAGE_LIMIT,
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
const result = await api.estimateFees(
|
|
731
|
+
context,
|
|
732
|
+
createSendTransactionIntentMockData({
|
|
733
|
+
type: 'send',
|
|
734
|
+
sender: 'tz1test',
|
|
735
|
+
recipient: 'tz1recipient',
|
|
736
|
+
amount: 1000n,
|
|
737
|
+
asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' },
|
|
738
|
+
})
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
expect(logicEstimateFees).toHaveBeenCalledTimes(2)
|
|
742
|
+
expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT)
|
|
743
|
+
logicEstimateFees.mockReset()
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
it('does not answer a token estimate with native fallback constants', async () => {
|
|
747
|
+
// Native fallback limits under-provision an FA2 transfer, which then fails on chain.
|
|
748
|
+
logicEstimateFees.mockRejectedValue(new Error('Public key not found'))
|
|
749
|
+
|
|
750
|
+
await expect(
|
|
751
|
+
api.estimateFees(context, {
|
|
752
|
+
intentType: 'transaction',
|
|
753
|
+
type: 'send',
|
|
754
|
+
sender: 'tz1test',
|
|
755
|
+
recipient: 'tz1recipient',
|
|
756
|
+
amount: 1000n,
|
|
757
|
+
asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' },
|
|
758
|
+
})
|
|
759
|
+
).rejects.toThrow('Public key not found')
|
|
760
|
+
|
|
761
|
+
logicEstimateFees.mockReset()
|
|
762
|
+
})
|
|
763
|
+
|
|
587
764
|
it('should not throw for delegate.unchanged errors', async () => {
|
|
588
765
|
logicEstimateFees.mockResolvedValue({
|
|
589
766
|
estimatedFees: DEFAULT_ESTIMATED_FEES,
|
|
@@ -641,6 +818,37 @@ describe('estimateFees', () => {
|
|
|
641
818
|
})
|
|
642
819
|
})
|
|
643
820
|
|
|
821
|
+
it.each(MAPPED_TAQUITO_ERRORS)(
|
|
822
|
+
'should not throw for %s, which validateIntent maps',
|
|
823
|
+
async (id) => {
|
|
824
|
+
logicEstimateFees.mockResolvedValue({
|
|
825
|
+
estimatedFees: DEFAULT_ESTIMATED_FEES,
|
|
826
|
+
gasLimit: DEFAULT_GAS_LIMIT,
|
|
827
|
+
storageLimit: DEFAULT_STORAGE_LIMIT,
|
|
828
|
+
taquitoError: `proto.025-PsUshuai.${id}`,
|
|
829
|
+
})
|
|
830
|
+
const result = await api.estimateFees(
|
|
831
|
+
context,
|
|
832
|
+
createStakingTransactionIntentMockData({
|
|
833
|
+
type: 'stake',
|
|
834
|
+
mode: 'delegate',
|
|
835
|
+
sender: 'tz1test',
|
|
836
|
+
recipient: 'tz1validator',
|
|
837
|
+
amount: 1n,
|
|
838
|
+
valAddress: 'tz1validator',
|
|
839
|
+
})
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
expect(result).toEqual({
|
|
843
|
+
value: DEFAULT_ESTIMATED_FEES,
|
|
844
|
+
parameters: {
|
|
845
|
+
gasLimit: DEFAULT_GAS_LIMIT,
|
|
846
|
+
storageLimit: DEFAULT_STORAGE_LIMIT,
|
|
847
|
+
},
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
)
|
|
851
|
+
|
|
644
852
|
it('returns total estimation for unrevealed delegate when minFees applied (composite)', async () => {
|
|
645
853
|
const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V'
|
|
646
854
|
mockGetAccountByAddress.mockResolvedValueOnce({
|
package/src/api/index.ts
CHANGED
|
@@ -20,8 +20,6 @@ import type {
|
|
|
20
20
|
import { rejectBalanceOptions } from '@ledgerhq/coin-module-framework/api/getBalance/rejectBalanceOptions'
|
|
21
21
|
import { IncorrectTypeError } from '@ledgerhq/coin-module-framework/api/index'
|
|
22
22
|
import { craftTransactionData } from '@ledgerhq/coin-module-framework/logic/craftTransactionData'
|
|
23
|
-
import { log } from '@ledgerhq/logs'
|
|
24
|
-
import { getRevealFee } from '@taquito/taquito'
|
|
25
23
|
import { getPkhfromPk, validatePublicKey, ValidationResult } from '@taquito/utils'
|
|
26
24
|
import {
|
|
27
25
|
broadcast,
|
|
@@ -38,6 +36,7 @@ import {
|
|
|
38
36
|
rawEncode,
|
|
39
37
|
validateIntent,
|
|
40
38
|
} from '../logic'
|
|
39
|
+
import { getRevealFeeForEstimation } from '../logic/estimateFees'
|
|
41
40
|
import { getTezosToolkit } from '../logic/tezosToolkit'
|
|
42
41
|
import { validateAddress } from '../logic/validateAddress'
|
|
43
42
|
import { hasManagerKey } from '../network/types'
|
|
@@ -45,6 +44,7 @@ import { createTzktApi } from '../network/tzkt'
|
|
|
45
44
|
import {
|
|
46
45
|
DUST_MARGIN_MUTEZ,
|
|
47
46
|
hasEmptyBalance,
|
|
47
|
+
isMappedTaquitoError,
|
|
48
48
|
normalizePublicKeyForAddress,
|
|
49
49
|
parseTezosTokenAsset,
|
|
50
50
|
resolveTezosOperationMode,
|
|
@@ -107,6 +107,27 @@ function isTezosTransactionType(
|
|
|
107
107
|
): type is 'send' | 'delegate' | 'undelegate' | 'stake' | 'unstake' | 'finalize_unstake' {
|
|
108
108
|
return ['send', 'delegate', 'undelegate', 'stake', 'unstake', 'finalize_unstake'].includes(type)
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether a failed estimation is the `contract.manager.inconsistent_hash` case that the
|
|
112
|
+
* keyless retry exists for. Taquito surfaces it as an HTTP error whose body carries the
|
|
113
|
+
* protocol error id.
|
|
114
|
+
*/
|
|
115
|
+
function isInconsistentHashError(error: unknown): boolean {
|
|
116
|
+
if (typeof error !== 'object' || error === null) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
const parts: unknown[] = []
|
|
120
|
+
if ('message' in error) {
|
|
121
|
+
parts.push(error.message)
|
|
122
|
+
}
|
|
123
|
+
if ('body' in error) {
|
|
124
|
+
parts.push(error.body)
|
|
125
|
+
}
|
|
126
|
+
return parts.some(
|
|
127
|
+
(part) => typeof part === 'string' && part.includes('contract.manager.inconsistent_hash')
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
async function craft(
|
|
111
132
|
context: TezosContext,
|
|
112
133
|
transactionIntent: TransactionIntent,
|
|
@@ -160,25 +181,24 @@ async function craft(
|
|
|
160
181
|
const needsReveal = hasManagerKey(senderApiAcc) && !senderApiAcc.revealed
|
|
161
182
|
const totalFee = Number(fee.fees || '0')
|
|
162
183
|
|
|
163
|
-
const feesConfig = config.fees
|
|
164
184
|
const revealFeeForSplit = needsReveal
|
|
165
|
-
?
|
|
185
|
+
? getRevealFeeForEstimation(config, transactionIntent.sender)
|
|
166
186
|
: 0
|
|
167
187
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
// The device compares the sum of the batch fees against the single total it was handed, so the
|
|
189
|
+
// split has to close exactly.
|
|
190
|
+
const desiredTxFee =
|
|
191
|
+
!customFees && estimation.parameters?.txFee !== undefined
|
|
192
|
+
? Number(estimation.parameters.txFee)
|
|
193
|
+
: totalFee - revealFeeForSplit
|
|
194
|
+
const txFee = Math.min(Math.max(desiredTxFee, 0), totalFee)
|
|
195
|
+
const revealFee = totalFee - txFee
|
|
176
196
|
|
|
177
197
|
const txForCraft = {
|
|
178
198
|
type: mappedType,
|
|
179
199
|
recipient: transactionIntent.recipient,
|
|
180
200
|
amount: amountToUse,
|
|
181
|
-
fee: { ...fee, fees: txFee.toString() },
|
|
201
|
+
fee: { ...fee, fees: txFee.toString(), revealFees: revealFee.toString() },
|
|
182
202
|
...(tokenCraftInfo && {
|
|
183
203
|
contractAddress: tokenCraftInfo.contractAddress,
|
|
184
204
|
tokenId: tokenCraftInfo.tokenId,
|
|
@@ -304,21 +324,20 @@ async function estimate(
|
|
|
304
324
|
// it's not clear why this happens, it couldn't be further investigated
|
|
305
325
|
// so we fallback to make an estimation without the public key
|
|
306
326
|
// there is a test that covers this, see "fallback to an estimation without the public key" index-mainnet.integ.test.ts
|
|
307
|
-
|
|
327
|
+
//
|
|
328
|
+
// A token estimate retries only for that error. Otherwise the keyless retry throws
|
|
329
|
+
// PublicKeyNotFoundError, and the native-transfer fallback answering it under-provisions
|
|
330
|
+
// the FA2 transfer. Other modes still depend on reaching that fallback.
|
|
331
|
+
if (tezosModeForEstimate === 'send_token' && !isInconsistentHashError(error)) {
|
|
332
|
+
throw error
|
|
333
|
+
}
|
|
334
|
+
context.logger('estimate-error', 'error estimating fees, trying without pubkey', { error })
|
|
308
335
|
estimation = await logicEstimate()
|
|
309
336
|
}
|
|
310
337
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
!estimation.taquitoError.includes('subtraction_underflow') &&
|
|
315
|
-
!estimation.taquitoError.includes('balance_too_low') &&
|
|
316
|
-
!estimation.taquitoError.includes('script_rejected') &&
|
|
317
|
-
!estimation.taquitoError.includes(
|
|
318
|
-
'cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate'
|
|
319
|
-
)
|
|
320
|
-
) {
|
|
321
|
-
throw new Error(`Fees estimation failed: ${estimation.taquitoError}`)
|
|
338
|
+
const { taquitoError } = estimation
|
|
339
|
+
if (taquitoError && !isMappedTaquitoError(taquitoError)) {
|
|
340
|
+
throw new Error(`Fees estimation failed: ${taquitoError}`)
|
|
322
341
|
}
|
|
323
342
|
|
|
324
343
|
return {
|
|
@@ -333,6 +352,14 @@ async function estimate(
|
|
|
333
352
|
} catch (error: unknown) {
|
|
334
353
|
// Handle PublicKeyNotFoundError
|
|
335
354
|
if (error instanceof Error && error.message.includes('Public key not found')) {
|
|
355
|
+
// Everything below describes a native tez transfer. An FA2 transfer costs several times
|
|
356
|
+
// more gas and the amounts vary per contract, so these values silently under-provision
|
|
357
|
+
// it: the operation is crafted correctly and the chain rejects it with gas_exhausted,
|
|
358
|
+
// charging the fee.
|
|
359
|
+
if (tezosModeForEstimate === 'send_token') {
|
|
360
|
+
throw error
|
|
361
|
+
}
|
|
362
|
+
|
|
336
363
|
const apiAccount = await api.getAccountByAddress(transactionIntent.recipient)
|
|
337
364
|
const storageLimit =
|
|
338
365
|
!hasEmptyBalance(apiAccount) || transactionIntent.type === 'stake' ? 0n : 277n
|
|
@@ -358,11 +385,11 @@ async function estimate(
|
|
|
358
385
|
} catch {
|
|
359
386
|
// When estimation fails because the sender is unrevealed (PublicKeyNotFoundError),
|
|
360
387
|
// fallback to a conservative gas value suitable for typical new-account XTZ transfers.
|
|
361
|
-
//
|
|
388
|
+
// A standard transfer consumes about 2169 gas, so 2500 leaves a small margin.
|
|
362
389
|
// The fee is computed according to Taquito's calculation so it will satisfy the Tezos prefilter rule:
|
|
363
390
|
// total_fees >= ceil(100 + 0.1*total_gas + op_size)
|
|
364
391
|
// We use a base of 120 instead of 100 to mimic Taquito and minimize rejected low-fee ops.
|
|
365
|
-
const SAFE_FALLBACK_GAS = 2500
|
|
392
|
+
const SAFE_FALLBACK_GAS = 2500
|
|
366
393
|
const FALLBACK_OP_SIZE_BYTES = 154 // typical forged size for a simple XTZ transfer
|
|
367
394
|
txGasLimit = BigInt(SAFE_FALLBACK_GAS)
|
|
368
395
|
baseTxFee = BigInt(
|
|
@@ -374,7 +401,7 @@ async function estimate(
|
|
|
374
401
|
}
|
|
375
402
|
|
|
376
403
|
const revealFee = needsReveal
|
|
377
|
-
? BigInt(
|
|
404
|
+
? BigInt(getRevealFeeForEstimation(config, transactionIntent.sender))
|
|
378
405
|
: 0n
|
|
379
406
|
const totalFee = baseTxFee + revealFee
|
|
380
407
|
|
|
@@ -98,7 +98,7 @@ export async function craftRawOperations(
|
|
|
98
98
|
if (getPkhfromPk(normalizedPk) !== sender) {
|
|
99
99
|
throw new Error('craftRawOperations: public key does not match the sender address')
|
|
100
100
|
}
|
|
101
|
-
const reveal = await estimateRevealLimits(tezosToolkit, sender
|
|
101
|
+
const reveal = await estimateRevealLimits(context, tezosToolkit, sender)
|
|
102
102
|
contents.push({
|
|
103
103
|
kind: OpKind.REVEAL,
|
|
104
104
|
source: sender,
|
|
@@ -232,9 +232,8 @@ async function estimateOperations(
|
|
|
232
232
|
`craftRawOperations: expected ${expected} estimate(s) for ${operations.length} operation(s), got ${estimates.length}`
|
|
233
233
|
)
|
|
234
234
|
}
|
|
235
|
-
// Keep the trailing N estimates, dropping a prepended reveal estimate if present. The
|
|
236
|
-
//
|
|
237
|
-
// craftTransaction) rather than this batch-prepended value, which is discarded.
|
|
235
|
+
// Keep the trailing N estimates, dropping a prepended reveal estimate if present. The REVEAL
|
|
236
|
+
// op itself uses the config-clamped estimateRevealLimits value, not this one.
|
|
238
237
|
return estimates.slice(estimates.length - operations.length)
|
|
239
238
|
}
|
|
240
239
|
|
|
@@ -177,6 +177,36 @@ describe('craftTransaction', () => {
|
|
|
177
177
|
])
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
+
it('stamps the reveal fee the caller supplies, not the node estimate', async () => {
|
|
181
|
+
// The device asserts that the batch fees sum to the single total the caller reported, which a
|
|
182
|
+
// reveal priced from the node would break.
|
|
183
|
+
mockTezosToolkit.rpc.getContract.mockResolvedValue({ counter: '1' })
|
|
184
|
+
mockTezosToolkit.estimate.reveal.mockResolvedValue({
|
|
185
|
+
gasLimit: 271,
|
|
186
|
+
storageLimit: 0,
|
|
187
|
+
suggestedFeeMutez: 310,
|
|
188
|
+
})
|
|
189
|
+
currentConfig = {
|
|
190
|
+
...mockConfig,
|
|
191
|
+
fees: { ...mockConfig.fees, minFees: 0, minRevealGasLimit: 0 },
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const account = { address: 'tz1...' }
|
|
195
|
+
const transaction: TransactionInput = {
|
|
196
|
+
type: 'send',
|
|
197
|
+
recipient: 'tz2...',
|
|
198
|
+
amount: BigInt(1000),
|
|
199
|
+
fee: { fees: '100', gasLimit: '200', storageLimit: '300', revealFees: '334' },
|
|
200
|
+
}
|
|
201
|
+
const publicKey = { publicKey: 'publicKey', publicKeyHash: 'publicKeyHash' }
|
|
202
|
+
|
|
203
|
+
const result = await craftTransaction(context, account, transaction, publicKey)
|
|
204
|
+
|
|
205
|
+
expect(result.contents[0]).toEqual(
|
|
206
|
+
expect.objectContaining({ kind: OpKind.REVEAL, fee: '334', gas_limit: '271' })
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
180
210
|
describe('when suggested fee is above or below minFees floor', () => {
|
|
181
211
|
it('craft transaction when default estimation is greater than minFees', async () => {
|
|
182
212
|
mockTezosToolkit.rpc.getContract.mockResolvedValue({ counter: '1' })
|
|
@@ -13,6 +13,12 @@ export type TransactionFee = {
|
|
|
13
13
|
fees?: string
|
|
14
14
|
gasLimit?: string
|
|
15
15
|
storageLimit?: string
|
|
16
|
+
/**
|
|
17
|
+
* Fee to stamp on the leading reveal operation. The caller sets it so the batch fees sum to the
|
|
18
|
+
* total it already reported. Without it the reveal keeps its own node estimate, which that
|
|
19
|
+
* total never accounted for.
|
|
20
|
+
*/
|
|
21
|
+
revealFees?: string
|
|
16
22
|
}
|
|
17
23
|
export type TransactionType =
|
|
18
24
|
| 'OUT'
|
|
@@ -72,12 +78,11 @@ export async function craftTransaction(
|
|
|
72
78
|
const contents: OperationContents[] = []
|
|
73
79
|
|
|
74
80
|
if (publicKey !== undefined) {
|
|
75
|
-
const
|
|
76
|
-
const reveal = await estimateRevealLimits(tezosToolkit, address, feesConfig)
|
|
81
|
+
const reveal = await estimateRevealLimits(context, tezosToolkit, address)
|
|
77
82
|
|
|
78
83
|
contents.push({
|
|
79
84
|
kind: OpKind.REVEAL,
|
|
80
|
-
fee: reveal.fee.toString(),
|
|
85
|
+
fee: transaction.fee.revealFees ?? reveal.fee.toString(),
|
|
81
86
|
gas_limit: reveal.gasLimit.toString(),
|
|
82
87
|
storage_limit: reveal.storageLimit.toString(),
|
|
83
88
|
source: publicKey.publicKeyHash,
|