diff --git a/api/types.go b/api/types.go index 13294d4e..1525a77f 100644 --- a/api/types.go +++ b/api/types.go @@ -571,13 +571,21 @@ type AvailableVsCurrencies struct { type Eip1559Fee struct { MaxFeePerGas *Amount `json:"maxFeePerGas"` MaxPriorityFeePerGas *Amount `json:"maxPriorityFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate,omitempty"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate,omitempty"` } // Eip1559Fees type Eip1559Fees struct { - BaseFeePerGas *Amount `json:"baseFeePerGas,omitempty"` - Low *Eip1559Fee `json:"low,omitempty"` - Medium *Eip1559Fee `json:"medium,omitempty"` - High *Eip1559Fee `json:"high,omitempty"` - Instant *Eip1559Fee `json:"instant,omitempty"` + BaseFeePerGas *Amount `json:"baseFeePerGas,omitempty"` + Low *Eip1559Fee `json:"low,omitempty"` + Medium *Eip1559Fee `json:"medium,omitempty"` + High *Eip1559Fee `json:"high,omitempty"` + Instant *Eip1559Fee `json:"instant,omitempty"` + NetworkCongestion float64 `json:"networkCongestion,omitempty"` + LatestPriorityFeeRange []*Amount `json:"latestPriorityFeeRange,omitempty"` + HistoricalPriorityFeeRange []*Amount `json:"historicalPriorityFeeRange,omitempty"` + HistoricalBaseFeeRange []*Amount `json:"historicalBaseFeeRange,omitempty"` + PriorityFeeTrend string `json:"priorityFeeTrend,omitempty"` + BaseFeeTrend string `json:"baseFeeTrend,omitempty"` } diff --git a/bchain/coins/eth/alternativefeeprovider.go b/bchain/coins/eth/alternativefeeprovider.go index 066a2386..fab48b33 100644 --- a/bchain/coins/eth/alternativefeeprovider.go +++ b/bchain/coins/eth/alternativefeeprovider.go @@ -17,3 +17,9 @@ type alternativeFeeProvider struct { type alternativeFeeProviderInterface interface { GetEip1559Fees() (*bchain.Eip1559Fees, error) } + +func (p *alternativeFeeProvider) GetEip1559Fees() (*bchain.Eip1559Fees, error) { + p.mux.Lock() + defer p.mux.Unlock() + return p.eip1559Fees, nil +} diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 7c7e30ed..c0b00b78 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -110,6 +110,23 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification s.Timeout = time.Duration(c.RPCTimeout) * time.Second s.PushHandler = pushHandler + if s.ChainConfig.AlternativeEstimateFee == "1inch" { + if s.alternativeFeeProvider, err = NewOneInchFeesProvider(s, s.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("New1InchFeesProvider error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + s.alternativeFeeProvider = nil + } + } else if s.ChainConfig.AlternativeEstimateFee == "infura" { + if s.alternativeFeeProvider, err = NewInfuraFeesProvider(s, s.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("NewInfuraFeesProvider error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + s.alternativeFeeProvider = nil + } + } + if s.alternativeFeeProvider != nil { + glog.Info("Using alternative fee provider ", s.ChainConfig.AlternativeEstimateFee) + } + return s, nil } @@ -170,14 +187,6 @@ func (b *EthereumRPC) Initialize() error { return err } - if b.ChainConfig.AlternativeEstimateFee == "1inch" { - if b.alternativeFeeProvider, err = NewOneInchFeesProvider(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { - glog.Error("New1InchFeesProvider error ", err, " Reverting to default estimateFee functionality") - // disable AlternativeEstimateFee logic - b.alternativeFeeProvider = nil - } - } - glog.Info("rpc: block chain ", b.Network) return nil @@ -1043,6 +1052,9 @@ func (b *EthereumRPC) EthereumTypeGetEip1559Fees() (*bchain.Eip1559Fees, error) if err != nil { return nil, err } + if len(h.BaseFeePerGas) < blocks { + return nil, nil + } hs, _ := json.Marshal(h) baseFee, _ := hexutil.DecodeUint64(h.BaseFeePerGas[blocks-1]) diff --git a/bchain/coins/eth/infurafees.go b/bchain/coins/eth/infurafees.go new file mode 100644 index 00000000..448a8156 --- /dev/null +++ b/bchain/coins/eth/infurafees.go @@ -0,0 +1,190 @@ +package eth + +import ( + "bytes" + "encoding/json" + "math/big" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// https://gas.api.infura.io/v3/${api_key}/networks/1/suggestedGasFees returns +// { +// "low": { +// "suggestedMaxPriorityFeePerGas": "0.01128", +// "suggestedMaxFeePerGas": "9.919888552", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 60000 +// }, +// "medium": { +// "suggestedMaxPriorityFeePerGas": "1.148315423", +// "suggestedMaxFeePerGas": "15.317625653", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 45000 +// }, +// "high": { +// "suggestedMaxPriorityFeePerGas": "2", +// "suggestedMaxFeePerGas": "24.78979967", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 30000 +// }, +// "estimatedBaseFee": "9.908608552", +// "networkCongestion": 0.004, +// "latestPriorityFeeRange": [ +// "0.05", +// "4" +// ], +// "historicalPriorityFeeRange": [ +// "0.006381976", +// "155.777346207" +// ], +// "historicalBaseFeeRange": [ +// "9.243163495", +// "16.734915363" +// ], +// "priorityFeeTrend": "up", +// "baseFeeTrend": "up", +// "version": "0.0.1" +// } + +type infuraFeeResult struct { + MaxPriorityFeePerGas string `json:"suggestedMaxPriorityFeePerGas"` + MaxFeePerGas string `json:"suggestedMaxFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate"` +} + +type infuraFeesResult struct { + BaseFee string `json:"estimatedBaseFee"` + Low infuraFeeResult `json:"low"` + Medium infuraFeeResult `json:"medium"` + High infuraFeeResult `json:"high"` + NetworkCongestion float64 `json:"networkCongestion"` + LatestPriorityFeeRange []string `json:"latestPriorityFeeRange"` + HistoricalPriorityFeeRange []string `json:"historicalPriorityFeeRange"` + HistoricalBaseFeeRange []string `json:"historicalBaseFeeRange"` + PriorityFeeTrend string `json:"priorityFeeTrend"` + BaseFeeTrend string `json:"baseFeeTrend"` +} + +type infuraFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `json:"periodSeconds"` +} + +type infuraFeeProvider struct { + *alternativeFeeProvider + params infuraFeeParams + apiKey string +} + +// NewInfuraFeesProvider initializes https://gas.api.infura.io provider +func NewInfuraFeesProvider(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + p := &infuraFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} + err := json.Unmarshal([]byte(params), &p.params) + if err != nil { + return nil, err + } + if p.params.URL == "" || p.params.PeriodSeconds == 0 { + return nil, errors.New("NewInfuraFeesProvider: missing config parameters 'url' or 'periodSeconds'.") + } + p.apiKey = os.Getenv("INFURA_API_KEY") + if p.apiKey == "" { + return nil, errors.New("NewInfuraFeesProvider: missing INFURA_API_KEY env variable.") + } + p.params.URL = strings.Replace(p.params.URL, "${api_key}", p.apiKey, -1) + p.chain = chain + go p.FeeDownloader() + return p, nil +} + +func (p *infuraFeeProvider) FeeDownloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + for { + var data infuraFeesResult + err := p.getData(&data) + if err != nil { + glog.Error("infuraFeeProvider.FeeDownloader", err) + } else { + p.processData(&data) + } + <-timer.C + timer.Reset(period) + } +} + +func bigIntFromFloatString(s string) *big.Int { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil + } + return big.NewInt(int64(f * 1e9)) +} + +func infuraFeesFromResult(result *infuraFeeResult) *bchain.Eip1559Fee { + fee := bchain.Eip1559Fee{} + fee.MaxFeePerGas = bigIntFromFloatString(result.MaxFeePerGas) + fee.MaxPriorityFeePerGas = bigIntFromFloatString(result.MaxPriorityFeePerGas) + fee.MinWaitTimeEstimate = result.MinWaitTimeEstimate + fee.MaxWaitTimeEstimate = result.MaxWaitTimeEstimate + return &fee +} + +func rangeFromString(feeRange []string) []*big.Int { + if feeRange == nil { + return nil + } + result := make([]*big.Int, len(feeRange)) + for i := range feeRange { + result[i] = bigIntFromFloatString(feeRange[i]) + } + return result +} + +func (p *infuraFeeProvider) processData(data *infuraFeesResult) bool { + fees := bchain.Eip1559Fees{} + fees.BaseFeePerGas = bigIntFromFloatString(data.BaseFee) + fees.High = infuraFeesFromResult(&data.High) + fees.Medium = infuraFeesFromResult(&data.Medium) + fees.Low = infuraFeesFromResult(&data.Low) + fees.NetworkCongestion = data.NetworkCongestion + fees.LatestPriorityFeeRange = rangeFromString(data.LatestPriorityFeeRange) + fees.HistoricalPriorityFeeRange = rangeFromString(data.HistoricalPriorityFeeRange) + fees.HistoricalBaseFeeRange = rangeFromString(data.HistoricalBaseFeeRange) + fees.PriorityFeeTrend = data.PriorityFeeTrend + fees.BaseFeeTrend = data.BaseFeeTrend + p.mux.Lock() + defer p.mux.Unlock() + p.lastSync = time.Now() + p.eip1559Fees = &fees + return true +} + +func (p *infuraFeeProvider) getData(res interface{}) error { + var httpData []byte + httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) + if err != nil { + return err + } + httpReq.Header.Set("Content-Type", "application/json") + httpRes, err := http.DefaultClient.Do(httpReq) + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + if httpRes.StatusCode != http.StatusOK { + return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) +} diff --git a/bchain/coins/eth/oneinchfees.go b/bchain/coins/eth/oneinchfees.go index d979f82d..e7bcecab 100644 --- a/bchain/coins/eth/oneinchfees.go +++ b/bchain/coins/eth/oneinchfees.go @@ -101,7 +101,7 @@ func bigIntFromString(s string) *big.Int { return b } -func feesFromResult(result *oneInchFeeFeeResult) *bchain.Eip1559Fee { +func oneInchFeesFromResult(result *oneInchFeeFeeResult) *bchain.Eip1559Fee { fee := bchain.Eip1559Fee{} fee.MaxFeePerGas = bigIntFromString(result.MaxFeePerGas) fee.MaxPriorityFeePerGas = bigIntFromString(result.MaxPriorityFeePerGas) @@ -111,15 +111,14 @@ func feesFromResult(result *oneInchFeeFeeResult) *bchain.Eip1559Fee { func (p *oneInchFeeProvider) processData(data *oneInchFeeFeesResult) bool { fees := bchain.Eip1559Fees{} fees.BaseFeePerGas = bigIntFromString(data.BaseFee) - fees.Instant = feesFromResult(&data.Instant) - fees.High = feesFromResult(&data.High) - fees.Medium = feesFromResult(&data.Medium) - fees.Low = feesFromResult(&data.Low) + fees.Instant = oneInchFeesFromResult(&data.Instant) + fees.High = oneInchFeesFromResult(&data.High) + fees.Medium = oneInchFeesFromResult(&data.Medium) + fees.Low = oneInchFeesFromResult(&data.Low) p.mux.Lock() defer p.mux.Unlock() p.lastSync = time.Now() p.eip1559Fees = &fees - glog.Infof("oneInchFeesProvider: %+v", p.eip1559Fees) return true } @@ -143,9 +142,3 @@ func (p *oneInchFeeProvider) getData(res interface{}) error { } return common.SafeDecodeResponseFromReader(httpRes.Body, &res) } - -func (p *oneInchFeeProvider) GetEip1559Fees() (*bchain.Eip1559Fees, error) { - p.mux.Lock() - defer p.mux.Unlock() - return p.eip1559Fees, nil -} diff --git a/bchain/types_ethereum_type.go b/bchain/types_ethereum_type.go index 6f807170..f1cb5d4e 100644 --- a/bchain/types_ethereum_type.go +++ b/bchain/types_ethereum_type.go @@ -168,13 +168,21 @@ type StakingPoolData struct { type Eip1559Fee struct { MaxFeePerGas *big.Int `json:"maxFeePerGas"` MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate,omitempty"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate,omitempty"` } // Eip1559Fees type Eip1559Fees struct { - BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"` - Low *Eip1559Fee `json:"low,omitempty"` - Medium *Eip1559Fee `json:"medium,omitempty"` - High *Eip1559Fee `json:"high,omitempty"` - Instant *Eip1559Fee `json:"instant,omitempty"` + BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"` + Low *Eip1559Fee `json:"low,omitempty"` + Medium *Eip1559Fee `json:"medium,omitempty"` + High *Eip1559Fee `json:"high,omitempty"` + Instant *Eip1559Fee `json:"instant,omitempty"` + NetworkCongestion float64 `json:"networkCongestion,omitempty"` + LatestPriorityFeeRange []*big.Int `json:"latestPriorityFeeRange,omitempty"` + HistoricalPriorityFeeRange []*big.Int `json:"historicalPriorityFeeRange,omitempty"` + HistoricalBaseFeeRange []*big.Int `json:"historicalBaseFeeRange,omitempty"` + PriorityFeeTrend string `json:"priorityFeeTrend,omitempty"` + BaseFeeTrend string `json:"baseFeeTrend,omitempty"` } diff --git a/configs/coins/bsc_archive.json b/configs/coins/bsc_archive.json index 0b460876..52616745 100644 --- a/configs/coins/bsc_archive.json +++ b/configs/coins/bsc_archive.json @@ -15,7 +15,7 @@ }, "ipc": { "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 + "rpc_timeout": 240 }, "backend": { "package_name": "backend-bsc-archive", @@ -58,9 +58,13 @@ "block_addresses_to_keep": 600, "additional_params": { "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/56/suggestedGasFees\", \"periodSeconds\": 20}", "mempoolTxTimeoutHours": 48, "processInternalTransactions": true, "queryBackendOnMempoolResync": false, + "disableMempoolSync": true, "fiat_rates": "coingecko", "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", "fiat_rates_params": "{\"coin\": \"binancecoin\",\"platformIdentifier\": \"binance-smart-chain\",\"platformVsCurrency\": \"bnb\",\"periodSeconds\": 900}", diff --git a/configs/coins/ethereum_archive.json b/configs/coins/ethereum_archive.json index e7e839ba..f9a868b8 100644 --- a/configs/coins/ethereum_archive.json +++ b/configs/coins/ethereum_archive.json @@ -60,8 +60,8 @@ "consensusNodeVersion": "http://localhost:7516/eth/v1/node/version", "address_aliases": true, "eip1559Fees": true, - "alternative_estimate_fee": "1inch", - "alternative_estimate_fee_params": "{\"url\": \"https://api.1inch.dev/gas-price/v1.5/1\", \"periodSeconds\": 20}", + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/1/suggestedGasFees\", \"periodSeconds\": 20}", "mempoolTxTimeoutHours": 48, "processInternalTransactions": true, "queryBackendOnMempoolResync": false, diff --git a/configs/coins/ethereum_testnet_holesky_archive.json b/configs/coins/ethereum_testnet_holesky_archive.json index 6dccbccd..e659c49d 100644 --- a/configs/coins/ethereum_testnet_holesky_archive.json +++ b/configs/coins/ethereum_testnet_holesky_archive.json @@ -61,6 +61,8 @@ "consensusNodeVersion": "http://localhost:17536/eth/v1/node/version", "address_aliases": true, "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/17000/suggestedGasFees\", \"periodSeconds\": 60}", "mempoolTxTimeoutHours": 12, "processInternalTransactions": true, "queryBackendOnMempoolResync": false, diff --git a/server/websocket.go b/server/websocket.go index a95e5f83..3dc30a54 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -637,13 +637,24 @@ func eip1559FeesToApi(fee *bchain.Eip1559Fee) *api.Eip1559Fee { return nil } apiFee := api.Eip1559Fee{} - if fee != nil { - apiFee.MaxFeePerGas = (*api.Amount)(fee.MaxFeePerGas) - apiFee.MaxPriorityFeePerGas = (*api.Amount)(fee.MaxPriorityFeePerGas) - } + apiFee.MaxFeePerGas = (*api.Amount)(fee.MaxFeePerGas) + apiFee.MaxPriorityFeePerGas = (*api.Amount)(fee.MaxPriorityFeePerGas) + apiFee.MaxWaitTimeEstimate = fee.MaxWaitTimeEstimate + apiFee.MinWaitTimeEstimate = fee.MinWaitTimeEstimate return &apiFee } +func eip1559FeeRangeToApi(feeRange []*big.Int) []*api.Amount { + if feeRange == nil { + return nil + } + apiFeeRange := make([]*api.Amount, len(feeRange)) + for i := range feeRange { + apiFeeRange[i] = (*api.Amount)(feeRange[i]) + } + return apiFeeRange +} + func (s *WebsocketServer) estimateFee(params []byte) (interface{}, error) { var r WsEstimateFeeReq err := json.Unmarshal(params, &r) @@ -679,6 +690,12 @@ func (s *WebsocketServer) estimateFee(params []byte) (interface{}, error) { eip1559Api.High = eip1559FeesToApi(eip1559.High) eip1559Api.Medium = eip1559FeesToApi(eip1559.Medium) eip1559Api.Low = eip1559FeesToApi(eip1559.Low) + eip1559Api.NetworkCongestion = eip1559.NetworkCongestion + eip1559Api.BaseFeeTrend = eip1559.BaseFeeTrend + eip1559Api.PriorityFeeTrend = eip1559.PriorityFeeTrend + eip1559Api.LatestPriorityFeeRange = eip1559FeeRangeToApi(eip1559.LatestPriorityFeeRange) + eip1559Api.HistoricalBaseFeeRange = eip1559FeeRangeToApi(eip1559.HistoricalBaseFeeRange) + eip1559Api.HistoricalPriorityFeeRange = eip1559FeeRangeToApi(eip1559.HistoricalPriorityFeeRange) } for i := range r.Blocks { res[i].FeePerUnit = fee.String()