From 0bf60488017885b840615533bd475b94e4072f8a Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 17 Feb 2025 13:25:08 +0100 Subject: [PATCH] feat(wallet)_: calculating tx estimated time --- .../connector/commands/send_transaction.go | 2 +- services/wallet/api.go | 5 + services/wallet/common/utils.go | 9 + services/wallet/router/fees/estimated_time.go | 134 +++++++++++++-- services/wallet/router/fees/fees.go | 55 +++--- services/wallet/router/fees/fees_test.go | 157 +++++++++++++++--- services/wallet/router/router.go | 41 ++--- services/wallet/router/router_helper.go | 21 +-- services/wallet/router/routes/router_path.go | 4 +- .../wallet/router/routes/router_path_test.go | 4 +- 10 files changed, 337 insertions(+), 95 deletions(-) diff --git a/services/connector/commands/send_transaction.go b/services/connector/commands/send_transaction.go index fe5be2daa..fe28ef656 100644 --- a/services/connector/commands/send_transaction.go +++ b/services/connector/commands/send_transaction.go @@ -97,7 +97,7 @@ func (c *SendTransactionCommand) Execute(ctx context.Context, request RPCRequest if !fetchedFees.EIP1559Enabled { params.GasPrice = (*hexutil.Big)(fetchedFees.GasPrice) } else { - maxFees, priorityFee, err := fetchedFees.FeeFor(fees.GasFeeMedium) + maxFees, priorityFee, _, err := fetchedFees.FeeFor(fees.GasFeeMedium) if err != nil { return "", err } diff --git a/services/wallet/api.go b/services/wallet/api.go index 288b49f05..cdc9db9e7 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -481,6 +481,11 @@ func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, return api.s.router.GetFeesManager().TransactionEstimatedTime(ctx, chainID, gweiToWei(maxFeePerGas)), nil } +func (api *API) GetTransactionEstimatedTimeV2(ctx context.Context, chainID uint64, maxFeePerGas *hexutil.Big, maxPriorityFeePerGas *hexutil.Big) (uint, error) { + logutils.ZapLogger().Debug("call to getTransactionEstimatedTimeV2") + return api.s.router.GetFeesManager().TransactionEstimatedTimeV2(ctx, chainID, maxFeePerGas.ToInt(), maxPriorityFeePerGas.ToInt()), nil +} + func gweiToWei(val *big.Float) *big.Int { res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil) return res diff --git a/services/wallet/common/utils.go b/services/wallet/common/utils.go index 793fb0a86..ae6fa2415 100644 --- a/services/wallet/common/utils.go +++ b/services/wallet/common/utils.go @@ -4,6 +4,7 @@ import ( "context" "math/big" "reflect" + "time" gethParams "github.com/ethereum/go-ethereum/params" "github.com/status-im/status-go/params" @@ -75,3 +76,11 @@ func WeiToGwei(val *big.Int) *big.Float { return result.Quo(result, new(big.Float).SetInt(unit)) } + +func GetBlockCreationTimeForChain(chainID uint64) time.Duration { + blockDuration, found := AverageBlockDurationForChain[ChainID(chainID)] + if !found { + blockDuration = AverageBlockDurationForChain[ChainID(UnknownChainID)] + } + return blockDuration +} diff --git a/services/wallet/router/fees/estimated_time.go b/services/wallet/router/fees/estimated_time.go index 9498ae2fb..5df28dd4f 100644 --- a/services/wallet/router/fees/estimated_time.go +++ b/services/wallet/router/fees/estimated_time.go @@ -5,10 +5,24 @@ import ( "math" "math/big" "sort" - "strings" + + "github.com/status-im/status-go/services/wallet/common" ) -const inclusionThreshold = 0.95 +const ( + inclusionThreshold = 0.95 + + priorityFeePercentileHigh = 0.6 + priorityFeePercentileMedium = 0.5 + priorityFeePercentileLow = 0.4 + + baseFeePercentileFirstBlock = 0.8 + baseFeePercentileSecondBlock = 0.7 + baseFeePercentileThirdBlock = 0.6 + baseFeePercentileFourthBlock = 0.5 + baseFeePercentileFifthBlock = 0.4 + baseFeePercentileSixthBlock = 0.3 +) type TransactionEstimation int @@ -30,8 +44,8 @@ func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint6 } func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int) TransactionEstimation { - fees, err := f.getFeeHistorySorted(feeHistory) - if err != nil || len(fees) == 0 { + fees := f.convertToBigIntAndSort(feeHistory.BaseFeePerGas) + if len(fees) == 0 { return Unknown } @@ -102,14 +116,110 @@ func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int return MoreThanFiveMinutes } -func (f *FeeManager) getFeeHistorySorted(feeHistory *FeeHistory) ([]*big.Int, error) { - fees := []*big.Int{} - for _, fee := range feeHistory.BaseFeePerGas { - i := new(big.Int) - i.SetString(strings.Replace(fee, "0x", "", 1), 16) - fees = append(fees, i) +func (f *FeeManager) convertToBigIntAndSort(hexArray []string) []*big.Int { + values := []*big.Int{} + for _, sValue := range hexArray { + iValue := new(big.Int) + _, ok := iValue.SetString(sValue, 0) + if !ok { + continue + } + values = append(values, iValue) } - sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 }) - return fees, nil + sort.Slice(values, func(i, j int) bool { return values[i].Cmp(values[j]) < 0 }) + return values +} + +func (f *FeeManager) getFeeHistoryForTimeEstimation(ctx context.Context, chainID uint64) (*FeeHistory, error) { + blockCount := uint64(10) // use the last 10 blocks for L1 chains + if chainID != common.EthereumMainnet && + chainID != common.EthereumSepolia && + chainID != common.AnvilMainnet { + blockCount = 50 // use the last 50 blocks for L2 chains + } + return f.getFeeHistory(ctx, chainID, blockCount, "latest", []int{RewardPercentiles2}) +} + +func calculateTimeForInclusion(chainID uint64, expectedInclusionInBlock int) uint { + blockCreationTime := common.GetBlockCreationTimeForChain(chainID) + blockCreationTimeInSeconds := uint(blockCreationTime.Seconds()) + + // the client will decide how to display estimated times, status-go sends it in the steps of 5 (for example the client + // should display "more than 1 minute" if the expected inclusion time is 60 seconds or more. + expectedInclusionTime := uint(expectedInclusionInBlock) * blockCreationTimeInSeconds + return (expectedInclusionTime/5 + 1) * 5 +} + +func getBaseFeePercentileIndex(sortedBaseFees []*big.Int, percentile float64, networkCongestion float64) int { + // calculate the index of the base fee for the given percentile corrected by the network congestion + index := int(float64(len(sortedBaseFees)) * percentile * networkCongestion) + if index >= len(sortedBaseFees) { + return len(sortedBaseFees) - 1 + } + return index +} + +// TransactionEstimatedTimeV2 returns the estimated time in seconds for a transaction to be included in a block +func (f *FeeManager) TransactionEstimatedTimeV2(ctx context.Context, chainID uint64, maxFeePerGas *big.Int, priorityFee *big.Int) uint { + feeHistory, err := f.getFeeHistoryForTimeEstimation(ctx, chainID) + if err != nil { + return 0 + } + + return f.estimatedTimeV2(feeHistory, maxFeePerGas, priorityFee, chainID) +} + +func (f *FeeManager) estimatedTimeV2(feeHistory *FeeHistory, txMaxFeePerGas *big.Int, txPriorityFee *big.Int, chainID uint64) uint { + sortedBaseFees := f.convertToBigIntAndSort(feeHistory.BaseFeePerGas) + if len(sortedBaseFees) == 0 { + return 0 + } + + var mediumPriorityFees []string // based on 50th percentile in the last 100 blocks + for _, fee := range feeHistory.Reward { + mediumPriorityFees = append(mediumPriorityFees, fee[0]) + } + mediumPriorityFeesSorted := f.convertToBigIntAndSort(mediumPriorityFees) + if len(mediumPriorityFeesSorted) == 0 { + return 0 + } + + txBaseFee := new(big.Int).Sub(txMaxFeePerGas, txPriorityFee) + + networkCongestion := calculateNetworkCongestion(feeHistory) + + // Priority fee for the first two blocks has to be higher than 60th percentile of the mediumPriorityFeesSorted + priorityFeePercentileIndex := int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileHigh) + priorityFeeForFirstTwoBlock := mediumPriorityFeesSorted[priorityFeePercentileIndex] + // Priority fee for the second two blocks has to be higher than 50th percentile of the mediumPriorityFeesSorted + priorityFeePercentileIndex = int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileMedium) + priorityFeeForSecondTwoBlocks := mediumPriorityFeesSorted[priorityFeePercentileIndex] + // Priority fee for the third two blocks has to be higher than 40th percentile of the mediumPriorityFeesSorted + priorityFeePercentileIndex = int(float64(len(mediumPriorityFeesSorted)) * priorityFeePercentileLow) + priorityFeeForThirdTwoBlocks := mediumPriorityFeesSorted[priorityFeePercentileIndex] + + // To include the transaction in the block `inclusionInBlock` its base fee has to be in a higher than `baseFeePercentile` + // and its priority fee has to be higher than the `priorityFee` + inclusions := []struct { + inclusionInBlock int + baseFeePercentile float64 + priorityFee *big.Int + }{ + {1, baseFeePercentileFirstBlock, priorityFeeForFirstTwoBlock}, + {2, baseFeePercentileSecondBlock, priorityFeeForFirstTwoBlock}, + {3, baseFeePercentileThirdBlock, priorityFeeForSecondTwoBlocks}, + {4, baseFeePercentileFourthBlock, priorityFeeForSecondTwoBlocks}, + {5, baseFeePercentileFifthBlock, priorityFeeForThirdTwoBlocks}, + {6, baseFeePercentileSixthBlock, priorityFeeForThirdTwoBlocks}, + } + + for _, p := range inclusions { + baseFeePercentileIndex := getBaseFeePercentileIndex(sortedBaseFees, p.baseFeePercentile, networkCongestion) + if txBaseFee.Cmp(sortedBaseFees[baseFeePercentileIndex]) >= 0 && txPriorityFee.Cmp(p.priorityFee) >= 0 { + return calculateTimeForInclusion(chainID, p.inclusionInBlock) + } + } + + return 0 } diff --git a/services/wallet/router/fees/fees.go b/services/wallet/router/fees/fees.go index ab1d26515..db4cccd57 100644 --- a/services/wallet/router/fees/fees.go +++ b/services/wallet/router/fees/fees.go @@ -32,28 +32,31 @@ var ( ) type MaxFeesLevels struct { - Low *hexutil.Big `json:"low"` - LowPriority *hexutil.Big `json:"lowPriority"` - Medium *hexutil.Big `json:"medium"` - MediumPriority *hexutil.Big `json:"mediumPriority"` - High *hexutil.Big `json:"high"` - HighPriority *hexutil.Big `json:"highPriority"` + Low *hexutil.Big `json:"low"` // Low max fee per gas in WEI + LowPriority *hexutil.Big `json:"lowPriority"` // Low priority fee in WEI + LowEstimatedTime uint `json:"lowEstimatedTime"` // Estimated time for low fees in seconds + Medium *hexutil.Big `json:"medium"` // Medium max fee per gas in WEI + MediumPriority *hexutil.Big `json:"mediumPriority"` // Medium priority fee in WEI + MediumEstimatedTime uint `json:"mediumEstimatedTime"` // Estimated time for medium fees in seconds + High *hexutil.Big `json:"high"` // High max fee per gas in WEI + HighPriority *hexutil.Big `json:"highPriority"` // High priority fee in WEI + HighEstimatedTime uint `json:"highEstimatedTime"` // Estimated time for high fees in seconds } type MaxPriorityFeesSuggestedBounds struct { - Lower *big.Int - Upper *big.Int + Lower *big.Int // Lower bound for priority fee per gas in WEI + Upper *big.Int // Upper bound for priority fee per gas in WEI } type SuggestedFees struct { - GasPrice *big.Int - BaseFee *big.Int - CurrentBaseFee *big.Int // Current network base fee (in ETH WEI) - MaxFeesLevels *MaxFeesLevels - MaxPriorityFeePerGas *big.Int // TODO: remove once clients stop using this field - MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds - L1GasFee *big.Float - EIP1559Enabled bool + GasPrice *big.Int // TODO: remove once clients stop using this field, used for EIP-1559 incompatible chains, not in use anymore + BaseFee *big.Int // TODO: remove once clients stop using this field, current network base fee (in ETH WEI), kept for backward compatibility + CurrentBaseFee *big.Int // Current network base fee (in ETH WEI) + MaxFeesLevels *MaxFeesLevels // Max fees levels for low, medium and high fee modes + MaxPriorityFeePerGas *big.Int // TODO: remove once clients stop using this field, kept for backward compatibility + MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds // Lower and upper bounds for priority fee per gas in WEI + L1GasFee *big.Float // TODO: remove once clients stop using this field, not in use anymore + EIP1559Enabled bool // TODO: remove it since all chains we have support EIP-1559 } // ////////////////////////////////////////////////////////////////////////////// @@ -71,23 +74,23 @@ type SuggestedFeesGwei struct { EIP1559Enabled bool `json:"eip1559Enabled"` } -func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, error) { +func (m *MaxFeesLevels) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, uint, error) { if mode == GasFeeCustom { - return nil, nil, ErrCustomFeeModeNotAvailableInSuggestedFees + return nil, nil, 0, ErrCustomFeeModeNotAvailableInSuggestedFees } if mode == GasFeeLow { - return m.Low.ToInt(), m.LowPriority.ToInt(), nil + return m.Low.ToInt(), m.LowPriority.ToInt(), m.LowEstimatedTime, nil } if mode == GasFeeHigh { - return m.High.ToInt(), m.HighPriority.ToInt(), nil + return m.High.ToInt(), m.HighPriority.ToInt(), m.MediumEstimatedTime, nil } - return m.Medium.ToInt(), m.MediumPriority.ToInt(), nil + return m.Medium.ToInt(), m.MediumPriority.ToInt(), m.HighEstimatedTime, nil } -func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, error) { +func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, *big.Int, uint, error) { return s.MaxFeesLevels.FeeFor(mode) } @@ -150,6 +153,14 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges } } + feeHistory, err = f.getFeeHistoryForTimeEstimation(ctx, chainID) + if err != nil { + return nil, err + } + suggestedFees.MaxFeesLevels.LowEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.Low.ToInt(), suggestedFees.MaxFeesLevels.LowPriority.ToInt(), chainID) + suggestedFees.MaxFeesLevels.MediumEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.Medium.ToInt(), suggestedFees.MaxFeesLevels.MediumPriority.ToInt(), chainID) + suggestedFees.MaxFeesLevels.HighEstimatedTime = f.estimatedTimeV2(feeHistory, suggestedFees.MaxFeesLevels.High.ToInt(), suggestedFees.MaxFeesLevels.HighPriority.ToInt(), chainID) + return suggestedFees, nil } diff --git a/services/wallet/router/fees/fees_test.go b/services/wallet/router/fees/fees_test.go index 0781865da..ae94d381f 100644 --- a/services/wallet/router/fees/fees_test.go +++ b/services/wallet/router/fees/fees_test.go @@ -64,32 +64,43 @@ func TestEstimatedTime(t *testing.T) { assert.Equal(t, LessThanOneMinute, estimation) } -func TestSuggestedFeesForNotEIP1559CompatibleChains(t *testing.T) { +func TestEstimatedTimeV2(t *testing.T) { state := setupTest(t) - - chainID := uint64(1) - gasPrice := big.NewInt(1) + // no fee history feeHistory := &FeeHistory{} - percentiles := []int{RewardPercentiles1, RewardPercentiles2, RewardPercentiles3} - state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", percentiles).Times(1).Return(nil) - mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl) - state.rpcClient.EXPECT().EthClient(chainID).Times(1).Return(mockedChainClient, nil) - mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(gasPrice, nil) + state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(10), "latest", []int{RewardPercentiles2}).Times(1).Return(nil) - suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID) - assert.NoError(t, err) - assert.NotNil(t, suggestedFees) - assert.Equal(t, gasPrice, suggestedFees.GasPrice) - assert.False(t, suggestedFees.EIP1559Enabled) -} + maxFeesPerGas := big.NewInt(2e9) + priorityFeesPerGas := big.NewInt(2e8) + estimation := state.feeManager.TransactionEstimatedTimeV2(context.Background(), uint64(1), maxFeesPerGas, priorityFeesPerGas) -func TestSuggestedFeesForEIP1559CompatibleChains(t *testing.T) { - state := setupTest(t) + assert.Equal(t, uint(0), estimation) - chainID := uint64(1) - feeHistory := &FeeHistory{} - percentiles := []int{RewardPercentiles1, RewardPercentiles2, RewardPercentiles3} - state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", percentiles).Times(1).Return(nil). + // there is fee history + state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(10), "latest", []int{RewardPercentiles2}).Times(1).Return(nil). + Do(func(feeHistory, chainID, method any, args ...any) { + feeHistoryResponse := &FeeHistory{ + BaseFeePerGas: []string{ + "0x12f0e070b", + "0x13f10da8b", + "0x126c30d5e", + "0x136e4fe51", + "0x134180d5a", + "0x134e32c33", + "0x137da8d22", + }, + } + *feeHistory.(*FeeHistory) = *feeHistoryResponse + }) + + maxFeesPerGas = big.NewInt(100e9) + priorityFeesPerGas = big.NewInt(10e9) + estimation = state.feeManager.TransactionEstimatedTimeV2(context.Background(), uint64(1), maxFeesPerGas, priorityFeesPerGas) + + assert.Equal(t, uint(0), estimation) + + // there is fee history and rewards + state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(10), "latest", []int{RewardPercentiles2}).Times(1).Return(nil). Do(func(feeHistory, chainID, method any, args ...any) { feeHistoryResponse := &FeeHistory{ BaseFeePerGas: []string{ @@ -152,6 +163,107 @@ func TestSuggestedFeesForEIP1559CompatibleChains(t *testing.T) { *feeHistory.(*FeeHistory) = *feeHistoryResponse }) + estimation = state.feeManager.TransactionEstimatedTimeV2(context.Background(), uint64(1), maxFeesPerGas, priorityFeesPerGas) + + assert.Equal(t, uint(15), estimation) +} + +func TestSuggestedFeesForNotEIP1559CompatibleChains(t *testing.T) { + state := setupTest(t) + + chainID := uint64(1) + gasPrice := big.NewInt(1) + feeHistory := &FeeHistory{} + percentiles := []int{RewardPercentiles1, RewardPercentiles2, RewardPercentiles3} + state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", percentiles).Times(1).Return(nil) + mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl) + state.rpcClient.EXPECT().EthClient(chainID).Times(1).Return(mockedChainClient, nil) + mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(gasPrice, nil) + + suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID) + assert.NoError(t, err) + assert.NotNil(t, suggestedFees) + assert.Equal(t, gasPrice, suggestedFees.GasPrice) + assert.False(t, suggestedFees.EIP1559Enabled) +} + +func TestSuggestedFeesForEIP1559CompatibleChains(t *testing.T) { + state := setupTest(t) + + feeHistoryResponse := &FeeHistory{ + BaseFeePerGas: []string{ + "0x12f0e070b", + "0x13f10da8b", + "0x126c30d5e", + "0x136e4fe51", + "0x134180d5a", + "0x134e32c33", + "0x137da8d22", + }, + GasUsedRatio: []float64{ + 0.7113286209349903, + 0.19531163333333335, + 0.7189235666666667, + 0.4639678021079083, + 0.5103012666666666, + 0.538413, + 0.16543626666666666, + }, + OldestBlock: "0x1497d4b", + Reward: [][]string{ + { + "0x2faf080", + "0x39d10680", + "0x722d7ef5", + }, + { + "0x5f5e100", + "0x3b9aca00", + "0x59682f00", + }, + { + "0x342e4a2", + "0x39d10680", + "0x77359400", + }, + { + "0x14a22237", + "0x40170350", + "0x77359400", + }, + { + "0x9134860", + "0x39d10680", + "0x618400ad", + }, + { + "0x2faf080", + "0x39d10680", + "0x77359400", + }, + { + "0x1ed69035", + "0x39d10680", + "0x41d0a8d6", + }, + }, + } + + chainID := uint64(1) + feeHistory := &FeeHistory{} + percentiles := []int{RewardPercentiles1, RewardPercentiles2, RewardPercentiles3} + state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", percentiles).Times(1).Return(nil). + Do(func(feeHistory, chainID, method any, args ...any) { + *feeHistory.(*FeeHistory) = *feeHistoryResponse + }) + + feeHistory = &FeeHistory{} + percentiles = []int{RewardPercentiles2} + state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(10), "latest", percentiles).Times(1).Return(nil). + Do(func(feeHistory, chainID, method any, args ...any) { + *feeHistory.(*FeeHistory) = *feeHistoryResponse + }) + suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID) assert.NoError(t, err) assert.NotNil(t, suggestedFees) @@ -177,4 +289,7 @@ func TestSuggestedFeesForEIP1559CompatibleChains(t *testing.T) { assert.Equal(t, big.NewInt(100000000), suggestedFees.MaxPriorityFeeSuggestedBounds.Lower) assert.Equal(t, big.NewInt(1915584245), suggestedFees.MaxPriorityFeeSuggestedBounds.Upper) assert.True(t, suggestedFees.EIP1559Enabled) + assert.Equal(t, uint(40), suggestedFees.MaxFeesLevels.LowEstimatedTime) + assert.Equal(t, uint(15), suggestedFees.MaxFeesLevels.MediumEstimatedTime) + assert.Equal(t, uint(15), suggestedFees.MaxFeesLevels.HighEstimatedTime) } diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go index 6973be945..815ebeefc 100644 --- a/services/wallet/router/router.go +++ b/services/wallet/router/router.go @@ -142,7 +142,7 @@ func (r *Router) SetTestBalanceMap(balanceMap map[string]*big.Int) { } } -func (r *Router) setCustomTxDetails(pathTxIdentity *requests.PathTxIdentity, pathTxCustomParams *requests.PathTxCustomParams) error { +func (r *Router) setCustomTxDetails(ctx context.Context, pathTxIdentity *requests.PathTxIdentity, pathTxCustomParams *requests.PathTxCustomParams) error { if pathTxIdentity == nil { return ErrTxIdentityNotProvided } @@ -164,31 +164,17 @@ func (r *Router) setCustomTxDetails(pathTxIdentity *requests.PathTxIdentity, pat return ErrCannotCustomizeIfNoRoute } + fetchedFees, err := r.feesManager.SuggestedFees(ctx, pathTxIdentity.ChainID) + if err != nil { + return err + } + for _, path := range r.activeRoutes.Best { if path.PathIdentity() != pathTxIdentity.PathIdentity() { continue } - if pathTxIdentity.IsApprovalTx { - path.ApprovalGasFeeMode = pathTxCustomParams.GasFeeMode - if pathTxCustomParams.GasFeeMode == fees.GasFeeCustom { - path.ApprovalTxNonce = (*hexutil.Uint64)(&pathTxCustomParams.Nonce) - path.ApprovalGasAmount = pathTxCustomParams.GasAmount - path.ApprovalMaxFeesPerGas = pathTxCustomParams.MaxFeesPerGas - path.ApprovalBaseFee = (*hexutil.Big)(new(big.Int).Sub(pathTxCustomParams.MaxFeesPerGas.ToInt(), pathTxCustomParams.PriorityFee.ToInt())) - path.ApprovalPriorityFee = pathTxCustomParams.PriorityFee - } - } else { - path.TxGasFeeMode = pathTxCustomParams.GasFeeMode - if pathTxCustomParams.GasFeeMode == fees.GasFeeCustom { - path.TxNonce = (*hexutil.Uint64)(&pathTxCustomParams.Nonce) - path.TxGasAmount = pathTxCustomParams.GasAmount - path.TxMaxFeesPerGas = pathTxCustomParams.MaxFeesPerGas - path.TxBaseFee = (*hexutil.Big)(new(big.Int).Sub(pathTxCustomParams.MaxFeesPerGas.ToInt(), pathTxCustomParams.PriorityFee.ToInt())) - path.TxPriorityFee = pathTxCustomParams.PriorityFee - } - } - + // update the custom params r.lastInputParamsMutex.Lock() if r.lastInputParams.PathTxCustomParams == nil { r.lastInputParams.PathTxCustomParams = make(map[string]*requests.PathTxCustomParams) @@ -196,6 +182,15 @@ func (r *Router) setCustomTxDetails(pathTxIdentity *requests.PathTxIdentity, pat r.lastInputParams.PathTxCustomParams[pathTxIdentity.TxIdentityKey()] = pathTxCustomParams r.lastInputParamsMutex.Unlock() + // update the path details + usedNonces := make(map[uint64]uint64) + err = r.evaluateAndUpdatePathDetails(ctx, path, fetchedFees, usedNonces, false, 0) + if err != nil { + return err + } + // inform the client about the changes + sendRouterResult(pathTxIdentity.RouterInputParamsUuid, r.activeRoutes, nil) + return nil } @@ -207,14 +202,14 @@ func (r *Router) SetFeeMode(ctx context.Context, pathTxIdentity *requests.PathTx return ErrCustomFeeModeCannotBeSetThisWay } - return r.setCustomTxDetails(pathTxIdentity, &requests.PathTxCustomParams{GasFeeMode: feeMode}) + return r.setCustomTxDetails(ctx, pathTxIdentity, &requests.PathTxCustomParams{GasFeeMode: feeMode}) } func (r *Router) SetCustomTxDetails(ctx context.Context, pathTxIdentity *requests.PathTxIdentity, pathTxCustomParams *requests.PathTxCustomParams) error { if pathTxCustomParams != nil && pathTxCustomParams.GasFeeMode != fees.GasFeeCustom { return ErrOnlyCustomFeeModeCanBeSetThisWay } - return r.setCustomTxDetails(pathTxIdentity, pathTxCustomParams) + return r.setCustomTxDetails(ctx, pathTxIdentity, pathTxCustomParams) } func newSuggestedRoutes( diff --git a/services/wallet/router/router_helper.go b/services/wallet/router/router_helper.go index b9a89d88a..f10ce489a 100644 --- a/services/wallet/router/router_helper.go +++ b/services/wallet/router/router_helper.go @@ -195,7 +195,7 @@ func (r *Router) applyCustomFields(ctx context.Context, path *routes.Path, fetch if r.lastInputParams.PathTxCustomParams == nil || len(r.lastInputParams.PathTxCustomParams) == 0 { // if no custom params are provided, use the initial fee mode - maxFeesPerGas, priorityFee, err := fetchedFees.FeeFor(r.lastInputParams.GasFeeMode) + maxFeesPerGas, priorityFee, estimatedTime, err := fetchedFees.FeeFor(r.lastInputParams.GasFeeMode) if err != nil { return err } @@ -204,31 +204,35 @@ func (r *Router) applyCustomFields(ctx context.Context, path *routes.Path, fetch path.ApprovalMaxFeesPerGas = (*hexutil.Big)(maxFeesPerGas) path.ApprovalBaseFee = (*hexutil.Big)(fetchedFees.BaseFee) path.ApprovalPriorityFee = (*hexutil.Big)(priorityFee) + path.ApprovalEstimatedTime = estimatedTime } path.TxGasFeeMode = r.lastInputParams.GasFeeMode path.TxMaxFeesPerGas = (*hexutil.Big)(maxFeesPerGas) path.TxBaseFee = (*hexutil.Big)(fetchedFees.BaseFee) path.TxPriorityFee = (*hexutil.Big)(priorityFee) + path.TxEstimatedTime = estimatedTime } else { if path.ApprovalRequired { approvalTxIdentityKey := path.TxIdentityKey(true) if approvalTxCustomParams, ok := r.lastInputParams.PathTxCustomParams[approvalTxIdentityKey]; ok { path.ApprovalGasFeeMode = approvalTxCustomParams.GasFeeMode if approvalTxCustomParams.GasFeeMode != fees.GasFeeCustom { - maxFeesPerGas, priorityFee, err := fetchedFees.FeeFor(approvalTxCustomParams.GasFeeMode) + maxFeesPerGas, priorityFee, estimatedTime, err := fetchedFees.FeeFor(approvalTxCustomParams.GasFeeMode) if err != nil { return err } path.ApprovalMaxFeesPerGas = (*hexutil.Big)(maxFeesPerGas) path.ApprovalBaseFee = (*hexutil.Big)(fetchedFees.BaseFee) path.ApprovalPriorityFee = (*hexutil.Big)(priorityFee) + path.ApprovalEstimatedTime = estimatedTime } else { path.ApprovalTxNonce = (*hexutil.Uint64)(&approvalTxCustomParams.Nonce) path.ApprovalGasAmount = approvalTxCustomParams.GasAmount path.ApprovalMaxFeesPerGas = approvalTxCustomParams.MaxFeesPerGas path.ApprovalBaseFee = (*hexutil.Big)(new(big.Int).Sub(approvalTxCustomParams.MaxFeesPerGas.ToInt(), approvalTxCustomParams.PriorityFee.ToInt())) path.ApprovalPriorityFee = approvalTxCustomParams.PriorityFee + path.ApprovalEstimatedTime = r.feesManager.TransactionEstimatedTimeV2(ctx, path.FromChain.ChainID, path.ApprovalMaxFeesPerGas.ToInt(), path.ApprovalPriorityFee.ToInt()) } } } @@ -237,19 +241,21 @@ func (r *Router) applyCustomFields(ctx context.Context, path *routes.Path, fetch if txCustomParams, ok := r.lastInputParams.PathTxCustomParams[txIdentityKey]; ok { path.TxGasFeeMode = txCustomParams.GasFeeMode if txCustomParams.GasFeeMode != fees.GasFeeCustom { - maxFeesPerGas, priorityFee, err := fetchedFees.FeeFor(txCustomParams.GasFeeMode) + maxFeesPerGas, priorityFee, estimatedTime, err := fetchedFees.FeeFor(txCustomParams.GasFeeMode) if err != nil { return err } path.TxMaxFeesPerGas = (*hexutil.Big)(maxFeesPerGas) path.TxBaseFee = (*hexutil.Big)(fetchedFees.BaseFee) path.TxPriorityFee = (*hexutil.Big)(priorityFee) + path.TxEstimatedTime = estimatedTime } else { path.TxNonce = (*hexutil.Uint64)(&txCustomParams.Nonce) path.TxGasAmount = txCustomParams.GasAmount path.TxMaxFeesPerGas = txCustomParams.MaxFeesPerGas path.TxBaseFee = (*hexutil.Big)(new(big.Int).Sub(txCustomParams.MaxFeesPerGas.ToInt(), txCustomParams.PriorityFee.ToInt())) path.TxPriorityFee = txCustomParams.PriorityFee + path.TxEstimatedTime = r.feesManager.TransactionEstimatedTimeV2(ctx, path.FromChain.ChainID, path.TxMaxFeesPerGas.ToInt(), path.TxPriorityFee.ToInt()) } } } @@ -336,15 +342,6 @@ func (r *Router) evaluateAndUpdatePathDetails(ctx context.Context, path *routes. path.RequiredTokenBalance = requiredTokenBalance path.RequiredNativeBalance = requiredNativeBalance - path.TxEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.TxMaxFeesPerGas.ToInt()) - if path.ApprovalRequired { - if path.TxMaxFeesPerGas.ToInt().Cmp(path.ApprovalMaxFeesPerGas.ToInt()) == 0 { - path.ApprovalEstimatedTime = path.TxEstimatedTime - } else { - path.ApprovalEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.ApprovalMaxFeesPerGas.ToInt()) - } - } - return } diff --git a/services/wallet/router/routes/router_path.go b/services/wallet/router/routes/router_path.go index 622a98566..7850ec47b 100644 --- a/services/wallet/router/routes/router_path.go +++ b/services/wallet/router/routes/router_path.go @@ -42,7 +42,7 @@ type Path struct { TxGasAmount uint64 // Gas used for the transaction TxBonderFees *hexutil.Big // Bonder fees for the transaction - used for Hop bridge (in selected token) TxTokenFees *hexutil.Big // Token fees for the transaction - used for bridges (represent the difference between the amount in and the amount out, in selected token) - TxEstimatedTime fees.TransactionEstimation + TxEstimatedTime uint // Estimated time for the transaction in seconds TxFee *hexutil.Big // fee for the transaction (includes tx fee only, doesn't include approval fees, l1 fees, l1 approval fees, token fees or bonders fees, in ETH WEI) TxL1Fee *hexutil.Big // L1 fee for the transaction - used for for transactions placed on L2 chains (in ETH WEI) @@ -57,7 +57,7 @@ type Path struct { ApprovalBaseFee *hexutil.Big // Base fee for the approval transaction (in ETH WEI) ApprovalPriorityFee *hexutil.Big // Priority fee for the approval transaction (in ETH WEI) ApprovalGasAmount uint64 // Gas used for the approval transaction - ApprovalEstimatedTime fees.TransactionEstimation + ApprovalEstimatedTime uint // Estimated time for the approval transaction in seconds ApprovalFee *hexutil.Big // Total fee for the approval transaction (includes approval tx fees only, doesn't include approval l1 fees, in ETH WEI) ApprovalL1Fee *hexutil.Big // L1 fee for the approval transaction - used for for transactions placed on L2 chains (in ETH WEI) diff --git a/services/wallet/router/routes/router_path_test.go b/services/wallet/router/routes/router_path_test.go index 866177b0d..256851713 100644 --- a/services/wallet/router/routes/router_path_test.go +++ b/services/wallet/router/routes/router_path_test.go @@ -36,7 +36,7 @@ func TestCopyPath(t *testing.T) { TxGasAmount: 100, TxBonderFees: (*hexutil.Big)(big.NewInt(100)), TxTokenFees: (*hexutil.Big)(big.NewInt(100)), - TxEstimatedTime: fees.TransactionEstimation(100), + TxEstimatedTime: 100, TxFee: (*hexutil.Big)(big.NewInt(100)), TxL1Fee: (*hexutil.Big)(big.NewInt(100)), ApprovalRequired: true, @@ -46,7 +46,7 @@ func TestCopyPath(t *testing.T) { ApprovalBaseFee: (*hexutil.Big)(big.NewInt(100)), ApprovalPriorityFee: (*hexutil.Big)(big.NewInt(100)), ApprovalGasAmount: 100, - ApprovalEstimatedTime: fees.TransactionEstimation(100), + ApprovalEstimatedTime: 100, ApprovalFee: (*hexutil.Big)(big.NewInt(100)), ApprovalL1Fee: (*hexutil.Big)(big.NewInt(100)), TxTotalFee: (*hexutil.Big)(big.NewInt(100)),