mirror of
https://github.com/status-im/status-go.git
synced 2025-01-17 18:22:13 +00:00
feat(wallet)_: added suggested min and max priority fee and current base fee for the path
- `Path` type extended with the following fields: - `SuggestedMinPriorityFee`, suggested min priority fee by the network - `SuggestedMaxPriorityFee`, suggested max priority fee by the network - `CurrentBaseFee`, current network base fee - The following wallet api endpoints marked as deprecated: - `GetSuggestedFees` - `GetTransactionEstimatedTime`
This commit is contained in:
parent
67134d9811
commit
90f4740add
@ -2,7 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
@ -13,6 +12,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
mock_client "github.com/status-im/status-go/rpc/chain/mock/client"
|
||||
"github.com/status-im/status-go/services/wallet/router/fees"
|
||||
"github.com/status-im/status-go/services/wallet/wallettypes"
|
||||
"github.com/status-im/status-go/signal"
|
||||
)
|
||||
@ -82,9 +82,10 @@ func TestSendTransactionWithSignalTimout(t *testing.T) {
|
||||
WalletResponseMaxInterval = 1 * time.Millisecond
|
||||
|
||||
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
|
||||
feeHistory := &fees.FeeHistory{}
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
|
||||
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
|
||||
|
||||
@ -127,9 +128,10 @@ func TestSendTransactionWithSignalAccepted(t *testing.T) {
|
||||
t.Cleanup(signal.ResetMobileSignalHandler)
|
||||
|
||||
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
|
||||
feeHistory := &fees.FeeHistory{}
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
|
||||
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
|
||||
|
||||
@ -169,9 +171,10 @@ func TestSendTransactionWithSignalRejected(t *testing.T) {
|
||||
t.Cleanup(signal.ResetMobileSignalHandler)
|
||||
|
||||
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
|
||||
feeHistory := &fees.FeeHistory{}
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
|
||||
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
|
||||
|
||||
|
@ -2,7 +2,6 @@ package connector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
@ -15,6 +14,7 @@ import (
|
||||
"github.com/status-im/status-go/services/connector/chainutils"
|
||||
"github.com/status-im/status-go/services/connector/commands"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/router/fees"
|
||||
"github.com/status-im/status-go/signal"
|
||||
)
|
||||
|
||||
@ -104,9 +104,10 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) {
|
||||
|
||||
// Send transaction
|
||||
mockedChainClient := mock_client.NewMockClientInterface(state.mockCtrl)
|
||||
feeHistory := &fees.FeeHistory{}
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil)
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().SuggestGasPrice(state.ctx).Times(1).Return(big.NewInt(1), nil)
|
||||
mockedChainClient.EXPECT().SuggestGasTipCap(state.ctx).Times(1).Return(big.NewInt(0), errors.New("EIP-1559 is not enabled"))
|
||||
state.rpcClient.EXPECT().EthClient(uint64(1)).Times(1).Return(mockedChainClient, nil)
|
||||
mockedChainClient.EXPECT().PendingNonceAt(state.ctx, common.Address(accountAddress)).Times(1).Return(uint64(10), nil)
|
||||
|
||||
|
@ -438,6 +438,7 @@ func (api *API) FetchTokenDetails(ctx context.Context, symbols []string) (map[st
|
||||
return api.s.marketManager.FetchTokenDetails(symbols)
|
||||
}
|
||||
|
||||
// @deprecated we should remove it once clients fully switched to wallet router, `GetSuggestedRoutesAsync` should be used instead
|
||||
func (api *API) GetSuggestedFees(ctx context.Context, chainID uint64) (*fees.SuggestedFeesGwei, error) {
|
||||
logutils.ZapLogger().Debug("call to GetSuggestedFees")
|
||||
return api.s.router.GetFeesManager().SuggestedFeesGwei(ctx, chainID)
|
||||
@ -448,6 +449,7 @@ func (api *API) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint6
|
||||
return api.s.blockChainState.GetEstimatedLatestBlockNumber(ctx, chainID)
|
||||
}
|
||||
|
||||
// @deprecated we should remove it once clients fully switched to wallet router, `GetSuggestedRoutesAsync` should be used instead
|
||||
func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) (fees.TransactionEstimation, error) {
|
||||
logutils.ZapLogger().Debug("call to getTransactionEstimatedTime")
|
||||
return api.s.router.GetFeesManager().TransactionEstimatedTime(ctx, chainID, gweiToWei(maxFeePerGas)), nil
|
||||
|
115
services/wallet/router/fees/estimated_time.go
Normal file
115
services/wallet/router/fees/estimated_time.go
Normal file
@ -0,0 +1,115 @@
|
||||
package fees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const inclusionThreshold = 0.95
|
||||
|
||||
type TransactionEstimation int
|
||||
|
||||
const (
|
||||
Unknown TransactionEstimation = iota
|
||||
LessThanOneMinute
|
||||
LessThanThreeMinutes
|
||||
LessThanFiveMinutes
|
||||
MoreThanFiveMinutes
|
||||
)
|
||||
|
||||
func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Int) TransactionEstimation {
|
||||
feeHistory, err := f.getFeeHistory(ctx, chainID, 100, "latest", nil)
|
||||
if err != nil {
|
||||
return Unknown
|
||||
}
|
||||
|
||||
return f.estimatedTime(feeHistory, maxFeePerGas)
|
||||
}
|
||||
|
||||
func (f *FeeManager) estimatedTime(feeHistory *FeeHistory, maxFeePerGas *big.Int) TransactionEstimation {
|
||||
fees, err := f.getFeeHistorySorted(feeHistory)
|
||||
if err != nil || len(fees) == 0 {
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// pEvent represents the probability of the transaction being included in a block,
|
||||
// we assume this one is static over time, in reality it is not.
|
||||
pEvent := 0.0
|
||||
for idx, fee := range fees {
|
||||
if fee.Cmp(maxFeePerGas) == 1 || idx == len(fees)-1 {
|
||||
pEvent = float64(idx) / float64(len(fees))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Probability of next 4 blocks including the transaction (less than 1 minute)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 4 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability := pEvent*4 - 6*(math.Pow(pEvent, 2)) + 4*(math.Pow(pEvent, 3)) - (math.Pow(pEvent, 4))
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanOneMinute
|
||||
}
|
||||
|
||||
// Probability of next 12 blocks including the transaction (less than 5 minutes)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability = pEvent*12 -
|
||||
66*(math.Pow(pEvent, 2)) +
|
||||
220*(math.Pow(pEvent, 3)) -
|
||||
495*(math.Pow(pEvent, 4)) +
|
||||
792*(math.Pow(pEvent, 5)) -
|
||||
924*(math.Pow(pEvent, 6)) +
|
||||
792*(math.Pow(pEvent, 7)) -
|
||||
495*(math.Pow(pEvent, 8)) +
|
||||
220*(math.Pow(pEvent, 9)) -
|
||||
66*(math.Pow(pEvent, 10)) +
|
||||
12*(math.Pow(pEvent, 11)) -
|
||||
math.Pow(pEvent, 12)
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanThreeMinutes
|
||||
}
|
||||
|
||||
// Probability of next 20 blocks including the transaction (less than 5 minutes)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability = pEvent*20 -
|
||||
190*(math.Pow(pEvent, 2)) +
|
||||
1140*(math.Pow(pEvent, 3)) -
|
||||
4845*(math.Pow(pEvent, 4)) +
|
||||
15504*(math.Pow(pEvent, 5)) -
|
||||
38760*(math.Pow(pEvent, 6)) +
|
||||
77520*(math.Pow(pEvent, 7)) -
|
||||
125970*(math.Pow(pEvent, 8)) +
|
||||
167960*(math.Pow(pEvent, 9)) -
|
||||
184756*(math.Pow(pEvent, 10)) +
|
||||
167960*(math.Pow(pEvent, 11)) -
|
||||
125970*(math.Pow(pEvent, 12)) +
|
||||
77520*(math.Pow(pEvent, 13)) -
|
||||
38760*(math.Pow(pEvent, 14)) +
|
||||
15504*(math.Pow(pEvent, 15)) -
|
||||
4845*(math.Pow(pEvent, 16)) +
|
||||
1140*(math.Pow(pEvent, 17)) -
|
||||
190*(math.Pow(pEvent, 18)) +
|
||||
20*(math.Pow(pEvent, 19)) -
|
||||
math.Pow(pEvent, 20)
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanFiveMinutes
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
|
||||
return fees, nil
|
||||
}
|
@ -2,16 +2,11 @@ package fees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus/misc"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
|
||||
"github.com/status-im/status-go/errors"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/rpc/chain"
|
||||
@ -29,6 +24,8 @@ const (
|
||||
|
||||
var (
|
||||
ErrCustomFeeModeNotAvailableInSuggestedFees = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-001"), Details: "custom fee mode is not available in suggested fees"}
|
||||
ErrEIP1559IncompaibleChain = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-002"), Details: "EIP-1559 is not supported on this chain"}
|
||||
ErrInvalidRewardData = &errors.ErrorResponse{Code: errors.ErrorCode("WRF-003"), Details: "invalid reward data"}
|
||||
)
|
||||
|
||||
type MaxFeesLevels struct {
|
||||
@ -37,13 +34,20 @@ type MaxFeesLevels struct {
|
||||
High *hexutil.Big `json:"high"`
|
||||
}
|
||||
|
||||
type MaxPriorityFeesSuggestedBounds struct {
|
||||
Lower *big.Int
|
||||
Upper *big.Int
|
||||
}
|
||||
|
||||
type SuggestedFees struct {
|
||||
GasPrice *big.Int `json:"gasPrice"`
|
||||
BaseFee *big.Int `json:"baseFee"`
|
||||
MaxFeesLevels *MaxFeesLevels `json:"maxFeesLevels"`
|
||||
MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"`
|
||||
L1GasFee *big.Float `json:"l1GasFee,omitempty"`
|
||||
EIP1559Enabled bool `json:"eip1559Enabled"`
|
||||
GasPrice *big.Int
|
||||
BaseFee *big.Int
|
||||
CurrentBaseFee *big.Int // Current network base fee (in ETH WEI)
|
||||
MaxFeesLevels *MaxFeesLevels
|
||||
MaxPriorityFeePerGas *big.Int
|
||||
MaxPriorityFeeSuggestedBounds *MaxPriorityFeesSuggestedBounds
|
||||
L1GasFee *big.Float
|
||||
EIP1559Enabled bool
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
@ -81,58 +85,29 @@ func (s *SuggestedFees) FeeFor(mode GasFeeMode) (*big.Int, error) {
|
||||
return s.MaxFeesLevels.FeeFor(mode)
|
||||
}
|
||||
|
||||
const inclusionThreshold = 0.95
|
||||
|
||||
type TransactionEstimation int
|
||||
|
||||
const (
|
||||
Unknown TransactionEstimation = iota
|
||||
LessThanOneMinute
|
||||
LessThanThreeMinutes
|
||||
LessThanFiveMinutes
|
||||
MoreThanFiveMinutes
|
||||
)
|
||||
|
||||
type FeeHistory struct {
|
||||
BaseFeePerGas []string `json:"baseFeePerGas"`
|
||||
}
|
||||
|
||||
type FeeManager struct {
|
||||
RPCClient rpc.ClientInterface
|
||||
}
|
||||
|
||||
func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*SuggestedFees, error) {
|
||||
backend, err := f.RPCClient.EthClient(chainID)
|
||||
feeHistory, err := f.getFeeHistory(ctx, chainID, 300, "latest", []int{25, 50, 75})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return f.getNonEIP1559SuggestedFees(ctx, chainID)
|
||||
}
|
||||
gasPrice, err := backend.SuggestGasPrice(ctx)
|
||||
|
||||
maxPriorityFeePerGasLowerBound, maxPriorityFeePerGas, maxPriorityFeePerGasUpperBound, baseFee, err := getEIP1559SuggestedFees(feeHistory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxPriorityFeePerGas, err := backend.SuggestGasTipCap(ctx)
|
||||
if err != nil {
|
||||
return &SuggestedFees{
|
||||
GasPrice: gasPrice,
|
||||
BaseFee: big.NewInt(0),
|
||||
MaxPriorityFeePerGas: big.NewInt(0),
|
||||
MaxFeesLevels: &MaxFeesLevels{
|
||||
Low: (*hexutil.Big)(gasPrice),
|
||||
Medium: (*hexutil.Big)(gasPrice),
|
||||
High: (*hexutil.Big)(gasPrice),
|
||||
},
|
||||
EIP1559Enabled: false,
|
||||
}, nil
|
||||
}
|
||||
baseFee, err := f.getBaseFee(ctx, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return f.getNonEIP1559SuggestedFees(ctx, chainID)
|
||||
}
|
||||
|
||||
return &SuggestedFees{
|
||||
GasPrice: gasPrice,
|
||||
BaseFee: baseFee,
|
||||
CurrentBaseFee: baseFee,
|
||||
MaxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
MaxPriorityFeeSuggestedBounds: &MaxPriorityFeesSuggestedBounds{
|
||||
Lower: maxPriorityFeePerGasLowerBound,
|
||||
Upper: maxPriorityFeePerGasUpperBound,
|
||||
},
|
||||
MaxFeesLevels: &MaxFeesLevels{
|
||||
Low: (*hexutil.Big)(new(big.Int).Add(baseFee, maxPriorityFeePerGas)),
|
||||
Medium: (*hexutil.Big)(new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), maxPriorityFeePerGas)),
|
||||
@ -142,6 +117,9 @@ func (f *FeeManager) SuggestedFees(ctx context.Context, chainID uint64) (*Sugges
|
||||
}, nil
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
// TODO: remove `SuggestedFeesGwei` once mobile app fully switched to router, this function should not be exposed via api
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
func (f *FeeManager) SuggestedFeesGwei(ctx context.Context, chainID uint64) (*SuggestedFeesGwei, error) {
|
||||
fees, err := f.SuggestedFees(ctx, chainID)
|
||||
if err != nil {
|
||||
@ -175,126 +153,3 @@ func (f *FeeManager) getBaseFee(ctx context.Context, client chain.ClientInterfac
|
||||
baseFee := misc.CalcBaseFee(config, header)
|
||||
return baseFee, nil
|
||||
}
|
||||
|
||||
func (f *FeeManager) TransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Int) TransactionEstimation {
|
||||
fees, err := f.getFeeHistorySorted(chainID)
|
||||
if err != nil {
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// pEvent represents the probability of the transaction being included in a block,
|
||||
// we assume this one is static over time, in reality it is not.
|
||||
pEvent := 0.0
|
||||
for idx, fee := range fees {
|
||||
if fee.Cmp(maxFeePerGas) == 1 || idx == len(fees)-1 {
|
||||
pEvent = float64(idx) / float64(len(fees))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Probability of next 4 blocks including the transaction (less than 1 minute)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 4 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability := pEvent*4 - 6*(math.Pow(pEvent, 2)) + 4*(math.Pow(pEvent, 3)) - (math.Pow(pEvent, 4))
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanOneMinute
|
||||
}
|
||||
|
||||
// Probability of next 12 blocks including the transaction (less than 5 minutes)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability = pEvent*12 -
|
||||
66*(math.Pow(pEvent, 2)) +
|
||||
220*(math.Pow(pEvent, 3)) -
|
||||
495*(math.Pow(pEvent, 4)) +
|
||||
792*(math.Pow(pEvent, 5)) -
|
||||
924*(math.Pow(pEvent, 6)) +
|
||||
792*(math.Pow(pEvent, 7)) -
|
||||
495*(math.Pow(pEvent, 8)) +
|
||||
220*(math.Pow(pEvent, 9)) -
|
||||
66*(math.Pow(pEvent, 10)) +
|
||||
12*(math.Pow(pEvent, 11)) -
|
||||
math.Pow(pEvent, 12)
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanThreeMinutes
|
||||
}
|
||||
|
||||
// Probability of next 20 blocks including the transaction (less than 5 minutes)
|
||||
// Generalising the formula: P(AUB) = P(A) + P(B) - P(A∩B) for 20 events and in our context P(A) == P(B) == pEvent
|
||||
// The factors are calculated using the combinations formula
|
||||
probability = pEvent*20 -
|
||||
190*(math.Pow(pEvent, 2)) +
|
||||
1140*(math.Pow(pEvent, 3)) -
|
||||
4845*(math.Pow(pEvent, 4)) +
|
||||
15504*(math.Pow(pEvent, 5)) -
|
||||
38760*(math.Pow(pEvent, 6)) +
|
||||
77520*(math.Pow(pEvent, 7)) -
|
||||
125970*(math.Pow(pEvent, 8)) +
|
||||
167960*(math.Pow(pEvent, 9)) -
|
||||
184756*(math.Pow(pEvent, 10)) +
|
||||
167960*(math.Pow(pEvent, 11)) -
|
||||
125970*(math.Pow(pEvent, 12)) +
|
||||
77520*(math.Pow(pEvent, 13)) -
|
||||
38760*(math.Pow(pEvent, 14)) +
|
||||
15504*(math.Pow(pEvent, 15)) -
|
||||
4845*(math.Pow(pEvent, 16)) +
|
||||
1140*(math.Pow(pEvent, 17)) -
|
||||
190*(math.Pow(pEvent, 18)) +
|
||||
20*(math.Pow(pEvent, 19)) -
|
||||
math.Pow(pEvent, 20)
|
||||
if probability >= inclusionThreshold {
|
||||
return LessThanFiveMinutes
|
||||
}
|
||||
|
||||
return MoreThanFiveMinutes
|
||||
}
|
||||
|
||||
func (f *FeeManager) getFeeHistorySorted(chainID uint64) ([]*big.Int, error) {
|
||||
var feeHistory FeeHistory
|
||||
|
||||
err := f.RPCClient.Call(&feeHistory, chainID, "eth_feeHistory", 101, "latest", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fees := []*big.Int{}
|
||||
for _, fee := range feeHistory.BaseFeePerGas {
|
||||
i := new(big.Int)
|
||||
i.SetString(strings.Replace(fee, "0x", "", 1), 16)
|
||||
fees = append(fees, i)
|
||||
}
|
||||
|
||||
sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
|
||||
return fees, nil
|
||||
}
|
||||
|
||||
// Returns L1 fee for placing a transaction to L1 chain, appicable only for txs made from L2.
|
||||
func (f *FeeManager) GetL1Fee(ctx context.Context, chainID uint64, input []byte) (uint64, error) {
|
||||
if chainID == common.EthereumMainnet || chainID == common.EthereumSepolia {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ethClient, err := f.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
contractAddress, err := gaspriceoracle.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
contract, err := gaspriceoracle.NewGaspriceoracleCaller(contractAddress, ethClient)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
callOpt := &bind.CallOpts{}
|
||||
|
||||
result, err := contract.GetL1Fee(callOpt, input)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.Uint64(), nil
|
||||
}
|
||||
|
71
services/wallet/router/fees/fees_history.go
Normal file
71
services/wallet/router/fees/fees_history.go
Normal file
@ -0,0 +1,71 @@
|
||||
package fees
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
|
||||
"github.com/status-im/status-go/services/wallet/common"
|
||||
)
|
||||
|
||||
type FeeHistory struct {
|
||||
BaseFeePerGas []string `json:"baseFeePerGas"`
|
||||
GasUsedRatio []float64 `json:"gasUsedRatio"`
|
||||
OldestBlock string `json:"oldestBlock"`
|
||||
Reward [][]string `json:"reward,omitempty"`
|
||||
}
|
||||
|
||||
func (fh *FeeHistory) isEIP1559Compatible() bool {
|
||||
if len(fh.BaseFeePerGas) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, fee := range fh.BaseFeePerGas {
|
||||
if fee != "0x0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FeeManager) getFeeHistory(ctx context.Context, chainID uint64, blockCount uint64, newestBlock string, rewardPercentiles []int) (feeHistory *FeeHistory, err error) {
|
||||
feeHistory = &FeeHistory{}
|
||||
err = f.RPCClient.Call(feeHistory, chainID, "eth_feeHistory", blockCount, newestBlock, rewardPercentiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feeHistory, nil
|
||||
}
|
||||
|
||||
// GetL1Fee returns L1 fee for placing a transaction to L1 chain, appicable only for txs made from L2.
|
||||
func (f *FeeManager) GetL1Fee(ctx context.Context, chainID uint64, input []byte) (uint64, error) {
|
||||
if chainID == common.EthereumMainnet || chainID == common.EthereumSepolia {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ethClient, err := f.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
contractAddress, err := gaspriceoracle.ContractAddress(chainID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
contract, err := gaspriceoracle.NewGaspriceoracleCaller(contractAddress, ethClient)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
callOpt := &bind.CallOpts{}
|
||||
|
||||
result, err := contract.GetL1Fee(callOpt, input)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.Uint64(), nil
|
||||
}
|
169
services/wallet/router/fees/fees_test.go
Normal file
169
services/wallet/router/fees/fees_test.go
Normal file
@ -0,0 +1,169 @@
|
||||
package fees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
mock_client "github.com/status-im/status-go/rpc/chain/mock/client"
|
||||
mock_rpcclient "github.com/status-im/status-go/rpc/mock/client"
|
||||
)
|
||||
|
||||
type testState struct {
|
||||
ctx context.Context
|
||||
mockCtrl *gomock.Controller
|
||||
rpcClient *mock_rpcclient.MockClientInterface
|
||||
feeManager *FeeManager
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T) (state testState) {
|
||||
state.ctx = context.Background()
|
||||
state.mockCtrl = gomock.NewController(t)
|
||||
state.rpcClient = mock_rpcclient.NewMockClientInterface(state.mockCtrl)
|
||||
state.feeManager = &FeeManager{
|
||||
RPCClient: state.rpcClient,
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func TestEstimatedTime(t *testing.T) {
|
||||
state := setupTest(t)
|
||||
// no fee history
|
||||
feeHistory := &FeeHistory{}
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(100), "latest", nil).Times(1).Return(nil)
|
||||
|
||||
maxFeesPerGas := big.NewInt(2e9)
|
||||
estimation := state.feeManager.TransactionEstimatedTime(context.Background(), uint64(1), maxFeesPerGas)
|
||||
|
||||
assert.Equal(t, Unknown, estimation)
|
||||
|
||||
// there is fee history
|
||||
state.rpcClient.EXPECT().Call(feeHistory, uint64(1), "eth_feeHistory", uint64(100), "latest", nil).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)
|
||||
estimation = state.feeManager.TransactionEstimatedTime(context.Background(), uint64(1), maxFeesPerGas)
|
||||
|
||||
assert.Equal(t, LessThanOneMinute, estimation)
|
||||
}
|
||||
|
||||
func TestSuggestedFeesForNotEIP1559CompatibleChains(t *testing.T) {
|
||||
state := setupTest(t)
|
||||
|
||||
chainID := uint64(1)
|
||||
gasPrice := big.NewInt(1)
|
||||
feeHistory := &FeeHistory{}
|
||||
|
||||
state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).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)
|
||||
|
||||
chainID := uint64(1)
|
||||
feeHistory := &FeeHistory{}
|
||||
|
||||
state.rpcClient.EXPECT().Call(feeHistory, chainID, "eth_feeHistory", uint64(300), "latest", []int{25, 50, 75}).Times(1).Return(nil).
|
||||
Do(func(feeHistory, chainID, method any, args ...any) {
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
*feeHistory.(*FeeHistory) = *feeHistoryResponse
|
||||
})
|
||||
|
||||
suggestedFees, err := state.feeManager.SuggestedFees(context.Background(), chainID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, suggestedFees)
|
||||
|
||||
assert.Nil(t, suggestedFees.GasPrice)
|
||||
assert.Equal(t, big.NewInt(6958609414), suggestedFees.BaseFee)
|
||||
assert.Equal(t, big.NewInt(6958609414), suggestedFees.CurrentBaseFee)
|
||||
assert.Equal(t, big.NewInt(7928609414), suggestedFees.MaxFeesLevels.Low.ToInt())
|
||||
assert.Equal(t, big.NewInt(14887218828), suggestedFees.MaxFeesLevels.Medium.ToInt())
|
||||
assert.Equal(t, big.NewInt(21845828242), suggestedFees.MaxFeesLevels.High.ToInt())
|
||||
assert.Equal(t, big.NewInt(970000000), suggestedFees.MaxPriorityFeePerGas)
|
||||
assert.Equal(t, big.NewInt(54715554), suggestedFees.MaxPriorityFeeSuggestedBounds.Lower)
|
||||
assert.Equal(t, big.NewInt(1636040877), suggestedFees.MaxPriorityFeeSuggestedBounds.Upper)
|
||||
assert.True(t, suggestedFees.EIP1559Enabled)
|
||||
}
|
138
services/wallet/router/fees/suggested_priority.go
Normal file
138
services/wallet/router/fees/suggested_priority.go
Normal file
@ -0,0 +1,138 @@
|
||||
package fees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
const baseFeeIncreaseFactor = 1.33
|
||||
|
||||
func hexStringToBigInt(value string) (*big.Int, error) {
|
||||
valueWitoutPrefix := strings.TrimPrefix(value, "0x")
|
||||
val, success := new(big.Int).SetString(valueWitoutPrefix, 16)
|
||||
if !success {
|
||||
return nil, errors.New("failed to convert hex string to big.Int")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func scaleBaseFeePerGas(value string) (*big.Int, error) {
|
||||
val, err := hexStringToBigInt(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueDouble := new(big.Float).SetInt(val)
|
||||
valueDouble.Mul(valueDouble, big.NewFloat(baseFeeIncreaseFactor))
|
||||
scaledValue := new(big.Int)
|
||||
valueDouble.Int(scaledValue)
|
||||
return scaledValue, nil
|
||||
}
|
||||
|
||||
func (f *FeeManager) getNonEIP1559SuggestedFees(ctx context.Context, chainID uint64) (*SuggestedFees, error) {
|
||||
backend, err := f.RPCClient.EthClient(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gasPrice, err := backend.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SuggestedFees{
|
||||
GasPrice: gasPrice,
|
||||
BaseFee: big.NewInt(0),
|
||||
MaxPriorityFeePerGas: big.NewInt(0),
|
||||
MaxPriorityFeeSuggestedBounds: &MaxPriorityFeesSuggestedBounds{
|
||||
Lower: big.NewInt(0),
|
||||
Upper: big.NewInt(0),
|
||||
},
|
||||
MaxFeesLevels: &MaxFeesLevels{
|
||||
Low: (*hexutil.Big)(gasPrice),
|
||||
Medium: (*hexutil.Big)(gasPrice),
|
||||
High: (*hexutil.Big)(gasPrice),
|
||||
},
|
||||
EIP1559Enabled: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getEIP1559SuggestedFees returns suggested fees for EIP-1559 compatible chains
|
||||
// source https://github.com/brave/brave-core/blob/master/components/brave_wallet/browser/eth_gas_utils.cc
|
||||
func getEIP1559SuggestedFees(feeHistory *FeeHistory) (lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee *big.Int, err error) {
|
||||
if feeHistory == nil || !feeHistory.isEIP1559Compatible() {
|
||||
return nil, nil, nil, nil, ErrEIP1559IncompaibleChain
|
||||
}
|
||||
|
||||
pendingBaseFee := feeHistory.BaseFeePerGas[len(feeHistory.BaseFeePerGas)-1]
|
||||
suggestedBaseFee, err = scaleBaseFeePerGas(pendingBaseFee)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
fallbackPriorityFee := big.NewInt(2e9) // 2 Gwei
|
||||
lowPriorityFee = new(big.Int).Set(fallbackPriorityFee)
|
||||
avgPriorityFee = new(big.Int).Set(fallbackPriorityFee)
|
||||
highPriorityFee = new(big.Int).Set(fallbackPriorityFee)
|
||||
|
||||
if len(feeHistory.Reward) == 0 {
|
||||
return lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee, nil
|
||||
}
|
||||
|
||||
priorityFees := make([][]*big.Int, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
currentPriorityFees := []*big.Int{}
|
||||
invalidData := false
|
||||
|
||||
for _, r := range feeHistory.Reward {
|
||||
if len(r) != 3 {
|
||||
invalidData = true
|
||||
break
|
||||
}
|
||||
|
||||
fee, err := hexStringToBigInt(r[i])
|
||||
if err != nil {
|
||||
invalidData = true
|
||||
break
|
||||
}
|
||||
currentPriorityFees = append(currentPriorityFees, fee)
|
||||
}
|
||||
|
||||
if invalidData {
|
||||
return nil, nil, nil, nil, ErrInvalidRewardData
|
||||
}
|
||||
|
||||
sort.Slice(currentPriorityFees, func(a, b int) bool {
|
||||
return currentPriorityFees[a].Cmp(currentPriorityFees[b]) < 0
|
||||
})
|
||||
|
||||
percentileIndex := int(float64(len(currentPriorityFees)) * 0.4)
|
||||
if i == 0 {
|
||||
lowPriorityFee = currentPriorityFees[percentileIndex]
|
||||
} else if i == 1 {
|
||||
avgPriorityFee = currentPriorityFees[percentileIndex]
|
||||
} else {
|
||||
highPriorityFee = currentPriorityFees[percentileIndex]
|
||||
}
|
||||
|
||||
priorityFees[i] = currentPriorityFees
|
||||
}
|
||||
|
||||
// Adjust low priority fee if it's equal to avg
|
||||
lowIndex := int(float64(len(priorityFees[0])) * 0.4)
|
||||
for lowIndex > 0 && lowPriorityFee == avgPriorityFee {
|
||||
lowIndex--
|
||||
lowPriorityFee = priorityFees[0][lowIndex]
|
||||
}
|
||||
|
||||
// Adjust high priority fee if it's equal to avg
|
||||
highIndex := int(float64(len(priorityFees[2])) * 0.4)
|
||||
for highIndex < len(priorityFees[2])-1 && highPriorityFee == avgPriorityFee {
|
||||
highIndex++
|
||||
highPriorityFee = priorityFees[2][highIndex]
|
||||
}
|
||||
|
||||
return lowPriorityFee, avgPriorityFee, highPriorityFee, suggestedBaseFee, nil
|
||||
}
|
@ -186,6 +186,19 @@ func (r *Router) applyCustomFields(ctx context.Context, path *routes.Path, fetch
|
||||
r.lastInputParamsMutex.Lock()
|
||||
defer r.lastInputParamsMutex.Unlock()
|
||||
|
||||
// set network fields
|
||||
if fetchedFees.CurrentBaseFee != nil {
|
||||
path.CurrentBaseFee = (*hexutil.Big)(fetchedFees.CurrentBaseFee)
|
||||
}
|
||||
if fetchedFees.MaxPriorityFeeSuggestedBounds != nil {
|
||||
if fetchedFees.MaxPriorityFeeSuggestedBounds.Lower != nil {
|
||||
path.SuggestedMinPriorityFee = (*hexutil.Big)(fetchedFees.MaxPriorityFeeSuggestedBounds.Lower)
|
||||
}
|
||||
if fetchedFees.MaxPriorityFeeSuggestedBounds.Upper != nil {
|
||||
path.SuggestedMaxPriorityFee = (*hexutil.Big)(fetchedFees.MaxPriorityFeeSuggestedBounds.Upper)
|
||||
}
|
||||
}
|
||||
|
||||
// set appropriate nonce/s, and update later in this function if custom nonce/s are provided
|
||||
err := r.resolveNonceForPath(ctx, path, r.lastInputParams.AddrFrom, usedNonces)
|
||||
if err != nil {
|
||||
@ -341,7 +354,11 @@ func (r *Router) evaluateAndUpdatePathDetails(ctx context.Context, path *routes.
|
||||
|
||||
path.TxEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.TxMaxFeesPerGas.ToInt())
|
||||
if path.ApprovalRequired {
|
||||
path.ApprovalEstimatedTime = r.feesManager.TransactionEstimatedTime(ctx, path.FromChain.ChainID, path.ApprovalMaxFeesPerGas.ToInt())
|
||||
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
|
||||
|
@ -22,7 +22,10 @@ type Path struct {
|
||||
AmountInLocked bool // Is the amount locked
|
||||
AmountOut *hexutil.Big // Amount that will be received on the destination chain
|
||||
|
||||
SuggestedLevelsForMaxFeesPerGas *fees.MaxFeesLevels // Suggested max fees for the transaction (in ETH WEI)
|
||||
SuggestedLevelsForMaxFeesPerGas *fees.MaxFeesLevels // Suggested max fees by the network (in ETH WEI)
|
||||
SuggestedMinPriorityFee *hexutil.Big // Suggested min priority fee by the network (in ETH WEI)
|
||||
SuggestedMaxPriorityFee *hexutil.Big // Suggested max priority fee by the network (in ETH WEI)
|
||||
CurrentBaseFee *hexutil.Big // Current network base fee (in ETH WEI)
|
||||
|
||||
TxNonce *hexutil.Uint64 // Nonce for the transaction
|
||||
TxMaxFeesPerGas *hexutil.Big // Max fees per gas (determined by client via GasFeeMode, in ETH WEI)
|
||||
@ -117,6 +120,18 @@ func (p *Path) Copy() *Path {
|
||||
}
|
||||
}
|
||||
|
||||
if p.SuggestedMinPriorityFee != nil {
|
||||
newPath.SuggestedMinPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedMinPriorityFee.ToInt()))
|
||||
}
|
||||
|
||||
if p.SuggestedMaxPriorityFee != nil {
|
||||
newPath.SuggestedMaxPriorityFee = (*hexutil.Big)(big.NewInt(0).Set(p.SuggestedMaxPriorityFee.ToInt()))
|
||||
}
|
||||
|
||||
if p.CurrentBaseFee != nil {
|
||||
newPath.CurrentBaseFee = (*hexutil.Big)(big.NewInt(0).Set(p.CurrentBaseFee.ToInt()))
|
||||
}
|
||||
|
||||
if p.TxNonce != nil {
|
||||
txNonce := *p.TxNonce
|
||||
newPath.TxNonce = &txNonce
|
||||
|
Loading…
x
Reference in New Issue
Block a user