@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
|
@@ -54,9 +54,19 @@ describe('estimateFees', () => {
|
|
|
54
54
|
contract: {
|
|
55
55
|
at: jest.fn(),
|
|
56
56
|
},
|
|
57
|
+
rpc: {
|
|
58
|
+
getConstants: jest.fn(),
|
|
59
|
+
},
|
|
57
60
|
setProvider: jest.fn(),
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
// Mainnet values; the two are equal, which is what makes a reveal + operation batch
|
|
64
|
+
// overflow the block limit unless the operation's gas is capped.
|
|
65
|
+
const MAINNET_GAS_CONSTANTS = {
|
|
66
|
+
hard_gas_limit_per_operation: 1040000,
|
|
67
|
+
hard_gas_limit_per_block: 1040000,
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
let currentConfig: TezosCoinConfig
|
|
61
71
|
const context: TezosContext = {
|
|
62
72
|
config: async () => currentConfig,
|
|
@@ -72,6 +82,7 @@ describe('estimateFees', () => {
|
|
|
72
82
|
jest
|
|
73
83
|
.mocked<DeepPartialReturn<typeof getTezosToolkit>>(getTezosToolkit)
|
|
74
84
|
.mockReturnValue(mockTezosToolkit)
|
|
85
|
+
mockTezosToolkit.rpc.getConstants.mockResolvedValue(MAINNET_GAS_CONSTANTS)
|
|
75
86
|
currentConfig = mkConfig(defaultFeesConfig)
|
|
76
87
|
})
|
|
77
88
|
|
|
@@ -312,7 +323,7 @@ describe('estimateFees', () => {
|
|
|
312
323
|
expect(result.amount).toBe(5000n)
|
|
313
324
|
})
|
|
314
325
|
|
|
315
|
-
it('useAllAmount stake
|
|
326
|
+
it('useAllAmount stake probes above the pseudotoken rate and resolves max to balance minus fees and reserve', async () => {
|
|
316
327
|
const suggestedFee = 700
|
|
317
328
|
const balance = 1_000_000n
|
|
318
329
|
mockTezosToolkit.estimate.stake.mockResolvedValue({
|
|
@@ -334,13 +345,48 @@ describe('estimateFees', () => {
|
|
|
334
345
|
})
|
|
335
346
|
|
|
336
347
|
expect(mockTezosToolkit.estimate.stake).toHaveBeenCalledWith({
|
|
337
|
-
amount:
|
|
348
|
+
amount: 1000,
|
|
338
349
|
mutez: true,
|
|
339
350
|
})
|
|
340
351
|
expect(result.fees).toBe(BigInt(suggestedFee))
|
|
341
352
|
expect(result.amount).toBe(balance - BigInt(suggestedFee) - 10_000n)
|
|
342
353
|
})
|
|
343
354
|
|
|
355
|
+
it('stake with no amount yet returns the fallback estimation without simulating', async () => {
|
|
356
|
+
const result = await estimateFees(context, {
|
|
357
|
+
account: revealedAccount,
|
|
358
|
+
transaction: {
|
|
359
|
+
mode: 'stake',
|
|
360
|
+
recipient: '',
|
|
361
|
+
amount: 0n,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
expect(mockTezosToolkit.estimate.stake).not.toHaveBeenCalled()
|
|
366
|
+
expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
|
|
367
|
+
expect(result.estimatedFees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
|
|
368
|
+
expect(result.gasLimit).toBe(BigInt(defaultFeesConfig.minGasLimit))
|
|
369
|
+
expect(result.storageLimit).toBe(BigInt(defaultFeesConfig.minStorageLimit))
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('stake with no amount yet adds the reveal fee to the fallback on unrevealed accounts', async () => {
|
|
373
|
+
const result = await estimateFees(context, {
|
|
374
|
+
account: unrevealedAccount,
|
|
375
|
+
transaction: {
|
|
376
|
+
mode: 'stake',
|
|
377
|
+
recipient: '',
|
|
378
|
+
amount: 0n,
|
|
379
|
+
},
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(mockTezosToolkit.estimate.stake).not.toHaveBeenCalled()
|
|
383
|
+
const expectedRevealFee = BigInt(getRevealFee(unrevealedAccount.address))
|
|
384
|
+
expect(result.fees).toBe(BigInt(defaultFeesConfig.minEstimatedFees))
|
|
385
|
+
expect(result.estimatedFees).toBe(
|
|
386
|
+
BigInt(defaultFeesConfig.minEstimatedFees) + expectedRevealFee
|
|
387
|
+
)
|
|
388
|
+
})
|
|
389
|
+
|
|
344
390
|
it('useAllAmount stake subtracts reveal fee from maxStakable on unrevealed accounts', async () => {
|
|
345
391
|
const suggestedFee = 700
|
|
346
392
|
const balance = 1_000_000n
|
|
@@ -620,6 +666,93 @@ describe('estimateFees', () => {
|
|
|
620
666
|
expect(result.estimatedFees).toBe(BigInt(400 + expectedRevealFee))
|
|
621
667
|
})
|
|
622
668
|
|
|
669
|
+
it('reports a validation-time protocol error as taquitoError instead of throwing', async () => {
|
|
670
|
+
const stakeAmountTooSmall = 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small'
|
|
671
|
+
mockTezosToolkit.estimate.stake.mockRejectedValue(
|
|
672
|
+
Object.assign(new Error('Http error response: (500)'), {
|
|
673
|
+
status: 500,
|
|
674
|
+
body: `[{"kind":"permanent","id":"${stakeAmountTooSmall}","amount":"1"}]`,
|
|
675
|
+
})
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
const result = await estimateFees(context, {
|
|
679
|
+
account: revealedAccount,
|
|
680
|
+
transaction: {
|
|
681
|
+
mode: 'stake',
|
|
682
|
+
recipient: '',
|
|
683
|
+
amount: 1n,
|
|
684
|
+
},
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
expect(result.taquitoError).toBe(stakeAmountTooSmall)
|
|
688
|
+
expect(result.fees).toBe(0n)
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('picks the mapped id out of a multi-error trace, not the generic one leading it', async () => {
|
|
692
|
+
const scriptRejected = 'proto.025-PsUshuai.michelson_v1.script_rejected'
|
|
693
|
+
mockTezosToolkit.estimate.transfer.mockRejectedValue(
|
|
694
|
+
Object.assign(new Error('Http error response: (500)'), {
|
|
695
|
+
status: 500,
|
|
696
|
+
body: JSON.stringify([
|
|
697
|
+
{ kind: 'temporary', id: 'proto.025-PsUshuai.michelson_v1.runtime_error' },
|
|
698
|
+
{ kind: 'temporary', id: scriptRejected, with: { string: 'nope' } },
|
|
699
|
+
]),
|
|
700
|
+
})
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
const result = await estimateFees(context, {
|
|
704
|
+
account: revealedAccount,
|
|
705
|
+
transaction: {
|
|
706
|
+
mode: 'send',
|
|
707
|
+
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
|
|
708
|
+
amount: 1000n,
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
expect(result.taquitoError).toBe(scriptRejected)
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('still finds the id when the body is not valid JSON', async () => {
|
|
716
|
+
const stakeAmountTooSmall = 'proto.025-PsUshuai.block_validation_plugin.stake_amount_too_small'
|
|
717
|
+
mockTezosToolkit.estimate.stake.mockRejectedValue(
|
|
718
|
+
Object.assign(new Error('Http error response: (500)'), {
|
|
719
|
+
status: 500,
|
|
720
|
+
body: `[{"kind":"permanent","id":"${stakeAmountTooSmall}","amount":"1"`,
|
|
721
|
+
})
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
const result = await estimateFees(context, {
|
|
725
|
+
account: revealedAccount,
|
|
726
|
+
transaction: {
|
|
727
|
+
mode: 'stake',
|
|
728
|
+
recipient: '',
|
|
729
|
+
amount: 1n,
|
|
730
|
+
},
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
expect(result.taquitoError).toBe(stakeAmountTooSmall)
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
it('rethrows a protocol error the mapping does not cover, so callers can still retry', async () => {
|
|
737
|
+
mockTezosToolkit.estimate.setDelegate.mockRejectedValue(
|
|
738
|
+
Object.assign(new Error('Http error response: (500)'), {
|
|
739
|
+
status: 500,
|
|
740
|
+
body: '[{"kind":"permanent","id":"proto.025-PsUshuai.gas_limit_too_high"}]',
|
|
741
|
+
})
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
await expect(
|
|
745
|
+
estimateFees(context, {
|
|
746
|
+
account: revealedAccount,
|
|
747
|
+
transaction: {
|
|
748
|
+
mode: 'delegate',
|
|
749
|
+
recipient: 'tz1validator',
|
|
750
|
+
amount: 0n,
|
|
751
|
+
},
|
|
752
|
+
})
|
|
753
|
+
).rejects.toThrow('Http error response: (500)')
|
|
754
|
+
})
|
|
755
|
+
|
|
623
756
|
it('send_token (FA2) revealed applies estimation from contract transfer', async () => {
|
|
624
757
|
const { transfer } = setupFa2ContractMock()
|
|
625
758
|
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
|
|
@@ -657,6 +790,95 @@ describe('estimateFees', () => {
|
|
|
657
790
|
expect(result.amount).toBe(100n)
|
|
658
791
|
})
|
|
659
792
|
|
|
793
|
+
it('send_token unrevealed caps gas so the reveal batch fits the block limit', async () => {
|
|
794
|
+
const { transfer } = setupFa2ContractMock()
|
|
795
|
+
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
|
|
796
|
+
mockTezosToolkit.estimate.transfer.mockResolvedValue({
|
|
797
|
+
suggestedFeeMutez: 600,
|
|
798
|
+
gasLimit: 4000,
|
|
799
|
+
storageLimit: 0,
|
|
800
|
+
burnFeeMutez: 0,
|
|
801
|
+
opSize: 250,
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
await estimateFees(context, {
|
|
805
|
+
account: unrevealedAccount,
|
|
806
|
+
transaction: {
|
|
807
|
+
mode: 'send_token',
|
|
808
|
+
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
|
|
809
|
+
amount: BigInt(100),
|
|
810
|
+
contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
|
|
811
|
+
tokenId: 0,
|
|
812
|
+
},
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
// min(1040000, 1040000 / 3): leaves room for the reveal Taquito prepends, so the
|
|
816
|
+
// simulation does not overflow hard_gas_limit_per_block.
|
|
817
|
+
expect(mockTezosToolkit.estimate.transfer).toHaveBeenCalledWith(
|
|
818
|
+
expect.objectContaining({ gasLimit: 346666 })
|
|
819
|
+
)
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
it('send_token derives the gas cap from protocol constants', async () => {
|
|
823
|
+
mockTezosToolkit.rpc.getConstants.mockResolvedValue({
|
|
824
|
+
hard_gas_limit_per_operation: 900000,
|
|
825
|
+
hard_gas_limit_per_block: 2400000,
|
|
826
|
+
})
|
|
827
|
+
const { transfer } = setupFa2ContractMock()
|
|
828
|
+
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
|
|
829
|
+
mockTezosToolkit.estimate.transfer.mockResolvedValue({
|
|
830
|
+
suggestedFeeMutez: 600,
|
|
831
|
+
gasLimit: 4000,
|
|
832
|
+
storageLimit: 0,
|
|
833
|
+
burnFeeMutez: 0,
|
|
834
|
+
opSize: 250,
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
await estimateFees(context, {
|
|
838
|
+
account: unrevealedAccount,
|
|
839
|
+
transaction: {
|
|
840
|
+
mode: 'send_token',
|
|
841
|
+
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
|
|
842
|
+
amount: BigInt(100),
|
|
843
|
+
contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
|
|
844
|
+
tokenId: 0,
|
|
845
|
+
},
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
// min(900000, 2400000 / 3): the block-derived bound is the tighter one here.
|
|
849
|
+
expect(mockTezosToolkit.estimate.transfer).toHaveBeenCalledWith(
|
|
850
|
+
expect.objectContaining({ gasLimit: 800000 })
|
|
851
|
+
)
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it('send_token revealed passes no gas cap', async () => {
|
|
855
|
+
const { transfer } = setupFa2ContractMock()
|
|
856
|
+
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
|
|
857
|
+
mockTezosToolkit.estimate.transfer.mockResolvedValue({
|
|
858
|
+
suggestedFeeMutez: 600,
|
|
859
|
+
gasLimit: 4000,
|
|
860
|
+
storageLimit: 0,
|
|
861
|
+
burnFeeMutez: 0,
|
|
862
|
+
opSize: 250,
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
await estimateFees(context, {
|
|
866
|
+
account: revealedAccount,
|
|
867
|
+
transaction: {
|
|
868
|
+
mode: 'send_token',
|
|
869
|
+
recipient: 'tz1VSUr8wwNhLAzempoch5d6nLRSNtxK8LBr',
|
|
870
|
+
amount: BigInt(100),
|
|
871
|
+
contractAddress: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU',
|
|
872
|
+
tokenId: 0,
|
|
873
|
+
},
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
// No reveal is prepended, so the batch cannot overflow and Taquito keeps its own default.
|
|
877
|
+
const params = mockTezosToolkit.estimate.transfer.mock.calls.at(-1)?.[0]
|
|
878
|
+
expect(params).not.toHaveProperty('gasLimit')
|
|
879
|
+
expect(mockTezosToolkit.rpc.getConstants).not.toHaveBeenCalled()
|
|
880
|
+
})
|
|
881
|
+
|
|
660
882
|
it('send_token unrevealed adds reveal fee to estimatedFees', async () => {
|
|
661
883
|
const { transfer } = setupFa2ContractMock()
|
|
662
884
|
mockTezosToolkit.contract.at.mockResolvedValue({ methods: { transfer } })
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import type { TezosCoinConfig, TezosContext } from '../config'
|
|
5
5
|
import type { TezosOperationMode } from '../types/model'
|
|
6
6
|
import type { Estimate } from '@taquito/taquito'
|
|
7
|
-
import { log } from '@ledgerhq/logs'
|
|
8
7
|
import { COST_PER_BYTE, getRevealFee, ORIGINATION_SIZE } from '@taquito/taquito'
|
|
9
8
|
import { validatePublicKey, ValidationResult } from '@taquito/utils'
|
|
10
9
|
import { UnsupportedTransactionMode } from '../types/errors'
|
|
@@ -13,10 +12,12 @@ import {
|
|
|
13
12
|
createFallbackEstimation,
|
|
14
13
|
createMockSigner,
|
|
15
14
|
DUST_MARGIN_MUTEZ,
|
|
15
|
+
isMappedTaquitoError,
|
|
16
16
|
MIN_SUGGESTED_FEE_SMALL_TRANSFER,
|
|
17
17
|
OP_SIZE_XTZ_TRANSFER,
|
|
18
18
|
normalizePublicKeyForAddress,
|
|
19
19
|
partitionNativeBalance,
|
|
20
|
+
STAKE_ESTIMATION_PROBE_MUTEZ,
|
|
20
21
|
} from '../utils'
|
|
21
22
|
import { getTezosToolkit } from './tezosToolkit'
|
|
22
23
|
|
|
@@ -106,7 +107,8 @@ export async function estimateFees(
|
|
|
106
107
|
transaction.mode === 'unstake')) ||
|
|
107
108
|
(amount === 0n && transaction.mode !== 'send_token')
|
|
108
109
|
if (coerceMinAmountForEstimation) {
|
|
109
|
-
|
|
110
|
+
// send/stake max or zero-amount pre-estimation (taquito refuses 0); not used for FA2 send_token
|
|
111
|
+
amount = transaction.mode === 'stake' ? STAKE_ESTIMATION_PROBE_MUTEZ : 1n
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
try {
|
|
@@ -119,6 +121,19 @@ export async function estimateFees(
|
|
|
119
121
|
...createFallbackEstimation(config),
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
const isStakeWithNoAmountYet =
|
|
125
|
+
transaction.mode === 'stake' && transaction.amount === 0n && !transaction.useAllAmount
|
|
126
|
+
if (isStakeWithNoAmountYet) {
|
|
127
|
+
const fallback = createFallbackEstimation(config)
|
|
128
|
+
const revealFee = account.revealed
|
|
129
|
+
? 0n
|
|
130
|
+
: BigInt(getRevealFeeForEstimation(config, account.address))
|
|
131
|
+
return {
|
|
132
|
+
...estimation,
|
|
133
|
+
...fallback,
|
|
134
|
+
estimatedFees: fallback.estimatedFees + revealFee,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
122
137
|
let estimate: Estimate
|
|
123
138
|
switch (transaction.mode) {
|
|
124
139
|
case 'send':
|
|
@@ -175,9 +190,13 @@ export async function estimateFees(
|
|
|
175
190
|
},
|
|
176
191
|
])
|
|
177
192
|
.toTransferParams({ mutez: true })
|
|
193
|
+
const revealBatchGasLimit = account.revealed
|
|
194
|
+
? undefined
|
|
195
|
+
: await gasLimitForRevealBatch(tezosToolkit)
|
|
178
196
|
estimate = await tezosToolkit.estimate.transfer({
|
|
179
197
|
...transferParams,
|
|
180
198
|
source: account.address,
|
|
199
|
+
...(revealBatchGasLimit !== undefined && { gasLimit: revealBatchGasLimit }),
|
|
181
200
|
})
|
|
182
201
|
break
|
|
183
202
|
}
|
|
@@ -210,7 +229,7 @@ export async function estimateFees(
|
|
|
210
229
|
const incr = DUST_MARGIN_MUTEZ * MINIMAL_FEE_PER_GAS_MUTEZ + Number(estimate.opSize)
|
|
211
230
|
const maxMinusBuff = maxAmount - (DUST_MARGIN_MUTEZ - incr)
|
|
212
231
|
estimation.amount = maxMinusBuff > 0 ? BigInt(maxMinusBuff) : 0n
|
|
213
|
-
|
|
232
|
+
context.logger('tezos-send-max', 'send-max fee inputs', {
|
|
214
233
|
minFees,
|
|
215
234
|
mainOpFee,
|
|
216
235
|
suggestedFeeMutez: estimate.suggestedFeeMutez,
|
|
@@ -240,14 +259,15 @@ export async function estimateFees(
|
|
|
240
259
|
if (typeof e !== 'object' || !e) throw e
|
|
241
260
|
if ('id' in e) {
|
|
242
261
|
estimation.taquitoError = String(e.id)
|
|
243
|
-
|
|
262
|
+
context.logger('taquito-error', 'taquito got error ' + String(e.id))
|
|
244
263
|
} else if ('status' in e) {
|
|
245
264
|
const errorMessage = 'message' in e ? String(e.message) : ''
|
|
265
|
+
const protocolErrorId = mappedProtocolErrorId(e)
|
|
246
266
|
if (
|
|
247
267
|
errorMessage.includes('Public key not found') ||
|
|
248
268
|
errorMessage.includes('wallet or contract API')
|
|
249
269
|
) {
|
|
250
|
-
|
|
270
|
+
context.logger(
|
|
251
271
|
'taquito-network-error',
|
|
252
272
|
'Recipient address not found (new account), using default fees',
|
|
253
273
|
{
|
|
@@ -297,8 +317,14 @@ export async function estimateFees(
|
|
|
297
317
|
// preserve input amount in fallback for readability/tests
|
|
298
318
|
estimation.amount = transaction.amount
|
|
299
319
|
}
|
|
320
|
+
} else if (protocolErrorId) {
|
|
321
|
+
estimation.taquitoError = protocolErrorId
|
|
322
|
+
context.logger(
|
|
323
|
+
'taquito-error',
|
|
324
|
+
'node refused the operation before applying it ' + protocolErrorId
|
|
325
|
+
)
|
|
300
326
|
} else {
|
|
301
|
-
|
|
327
|
+
context.logger('taquito-network-error', errorMessage, {
|
|
302
328
|
transaction: transaction,
|
|
303
329
|
})
|
|
304
330
|
throw e
|
|
@@ -352,7 +378,66 @@ export async function estimateFees(
|
|
|
352
378
|
return estimation
|
|
353
379
|
}
|
|
354
380
|
|
|
355
|
-
|
|
381
|
+
/**
|
|
382
|
+
* A protocol error raised while validating an operation comes back as an HTTP error whose body is
|
|
383
|
+
* the raw error array, so Taquito exposes no `id` of its own for `mapTaquitoErrors` to match.
|
|
384
|
+
*
|
|
385
|
+
* Only the ids that mapping covers are reported: any other has to keep propagating, because
|
|
386
|
+
* callers retry on a thrown estimation (see the without-public-key retry in `api/index.ts`).
|
|
387
|
+
*/
|
|
388
|
+
function mappedProtocolErrorId(error: object): string | undefined {
|
|
389
|
+
const body = 'body' in error && typeof error.body === 'string' ? error.body : ''
|
|
390
|
+
const message = 'message' in error ? String(error.message) : ''
|
|
391
|
+
// A trace lists the generic error first and the specific one last, the convention Taquito's
|
|
392
|
+
// own `TezosOperationError.lastError` relies on.
|
|
393
|
+
return protocolErrorIds(body || message)
|
|
394
|
+
.filter(isMappedTaquitoError)
|
|
395
|
+
.at(-1)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function protocolErrorIds(text: string): string[] {
|
|
399
|
+
try {
|
|
400
|
+
const parsed: unknown = JSON.parse(text)
|
|
401
|
+
const errors: unknown[] = Array.isArray(parsed) ? parsed : [parsed]
|
|
402
|
+
return errors.flatMap((error) =>
|
|
403
|
+
typeof error === 'object' && error !== null && 'id' in error && typeof error.id === 'string'
|
|
404
|
+
? [error.id]
|
|
405
|
+
: []
|
|
406
|
+
)
|
|
407
|
+
} catch {
|
|
408
|
+
return Array.from(text.matchAll(/"id"\s*:\s*"([^"]+)"/g), (match) => match[1])
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Gas limit to hand Taquito when the source account is unrevealed.
|
|
414
|
+
*
|
|
415
|
+
* Taquito prepends a reveal to any operation from an unrevealed account, then gives the
|
|
416
|
+
* operation itself `hard_gas_limit_per_operation` for the simulation. Mainnet sets
|
|
417
|
+
* `hard_gas_limit_per_block` to that same value, so the reveal's own gas tips the batch past
|
|
418
|
+
* the block limit and the node answers HTTP 500 `gas_exhausted.block`. Without a cap the
|
|
419
|
+
* estimation fails and the caller falls back to native-transfer constants, which are far too
|
|
420
|
+
* small for an FA2 transfer.
|
|
421
|
+
*
|
|
422
|
+
* Mirrors Taquito's own batch adjustment, `min(perOperation, perBlock / (opCount + 1))`, so
|
|
423
|
+
* the value tracks the protocol rather than a hardcoded figure.
|
|
424
|
+
*/
|
|
425
|
+
async function gasLimitForRevealBatch(
|
|
426
|
+
tezosToolkit: ReturnType<typeof getTezosToolkit>
|
|
427
|
+
): Promise<number> {
|
|
428
|
+
const constants = await tezosToolkit.rpc.getConstants()
|
|
429
|
+
const perOperation = Number(constants.hard_gas_limit_per_operation)
|
|
430
|
+
const perBlock = Number(constants.hard_gas_limit_per_block)
|
|
431
|
+
const REVEAL_PLUS_OPERATION = 2
|
|
432
|
+
return Math.floor(Math.min(perOperation, perBlock / (REVEAL_PLUS_OPERATION + 1)))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Reveal fee the estimation reports as part of the total, derived from Taquito's static per-curve
|
|
437
|
+
* table rather than a node estimate. The account is unrevealed and the public key is often absent
|
|
438
|
+
* at estimation time, so `estimate.reveal()` is not reliably available here.
|
|
439
|
+
*/
|
|
440
|
+
export function getRevealFeeForEstimation(config: TezosCoinConfig, address: string): number {
|
|
356
441
|
const minFees = config.fees.minFees ?? 0
|
|
357
442
|
return Math.max(minFees, getRevealFee(address))
|
|
358
443
|
}
|
|
@@ -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
|
}
|