From c01878a6cd13917ec3bf65a8b23eba104873b86c Mon Sep 17 00:00:00 2001 From: vctt Date: Mon, 4 Jul 2022 16:46:41 -0300 Subject: [PATCH] client: add external fee estimator as fallback, when feeEstimator fails on decred rpcwallets. --- client/asset/dcr/config.go | 1 + client/asset/dcr/dcr.go | 66 +++++++++++++++++++++++++++++++++++++- client/asset/interface.go | 2 ++ client/core/core.go | 1 + 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/client/asset/dcr/config.go b/client/asset/dcr/config.go index bfbe66c3d4..edae98977b 100644 --- a/client/asset/dcr/config.go +++ b/client/asset/dcr/config.go @@ -39,6 +39,7 @@ type walletConfig struct { FeeRateLimit float64 `ini:"feeratelimit"` RedeemConfTarget uint64 `ini:"redeemconftarget"` ActivelyUsed bool `ini:"special:activelyUsed"` //injected by core + ApiFeeFallback bool `ini:"apifeefallback"` } type rpcConfig struct { diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index a49c85c950..7c6c653759 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -9,9 +9,12 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "encoding/json" "errors" "fmt" + "io" "math" + "net/http" "path/filepath" "sort" "strconv" @@ -74,6 +77,12 @@ const ( // hierarchical deterministic key derivation for the internal branch of an // account. acctInternalBranch uint32 = 1 + + // externalApiUrl is the URL of the external API in case of fallback. + externalApiUrl = "https://explorer.dcrdata.org/insight/api" + // testnetExternalApiUrl is the URL of the testnet external API in case of + // fallback. + testnetExternalApiUrl = "https://testnet.dcrdata.org/insight/api" ) var ( @@ -172,6 +181,14 @@ var ( Description: "Path to the dcrwallet TLS certificate file", DefaultValue: defaultRPCCert, }, + { + Key: "apifeefallback", + DisplayName: "External fee rate estimates", + Description: "Allow fee rate estimation from a block explorer API. " + + "This is useful as a fallback for SPV wallets and RPC wallets " + + "that have recently been started.", + IsBoolean: true, + }, } spvOpts = []*asset.ConfigOption{{ @@ -215,6 +232,7 @@ var ( swapFeeBumpKey = "swapfeebump" splitKey = "swapsplit" redeemFeeBumpFee = "redeemfeebump" + client http.Client ) // outPoint is the hash and output index of a transaction output. @@ -499,6 +517,8 @@ type ExchangeWallet struct { feeRateLimit uint64 redeemConfTarget uint64 useSplitTx bool + ApiFeeFallback bool + Network dex.Network tipMtx sync.RWMutex currentTip *block @@ -656,6 +676,8 @@ func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParam feeRateLimit: feesLimitPerByte, redeemConfTarget: redeemConfTarget, useSplitTx: dcrCfg.UseSplitTx, + ApiFeeFallback: dcrCfg.ApiFeeFallback, + Network: cfg.Network, }, nil } @@ -874,7 +896,16 @@ func (dcr *ExchangeWallet) feeRate(confTarget uint64) (uint64, error) { } estimatedFeeRate, err := feeEstimator.EstimateSmartFeeRate(dcr.ctx, int64(confTarget), chainjson.EstimateSmartFeeConservative) if err != nil { - return 0, err + dcr.log.Errorf("Failed to get fee rate with estimate smart fee rate: %v", err) + if !dcr.ApiFeeFallback { + return 0, err + } + dcr.log.Debug("Retrieving fee rate from external API") + estimatedFeeRate, err = externalFeeEstimator(dcr.ctx, dcr.Network, confTarget) + if err != nil { + dcr.log.Errorf("Failed to get fee rate from external API: %v", err) + return 0, err + } } atomsPerKB, err := dcrutil.NewAmount(estimatedFeeRate) // atomsPerKB is 0 when err != nil if err != nil { @@ -885,6 +916,39 @@ func (dcr *ExchangeWallet) feeRate(confTarget uint64) (uint64, error) { return 1 + uint64(atomsPerKB)/1000, nil // dcrPerKB * 1e8 / 1e3 } +// externalFeeEstimator gets the fee rate from the external API +func externalFeeEstimator(ctx context.Context, net dex.Network, nb uint64) (float64, error) { + var url string + if net == dex.Testnet { + url = testnetExternalApiUrl + } else { + url = externalApiUrl + } + url = url + "/utils/estimatefee?nbBlocks=" + strconv.FormatUint(nb, 10) + ctx, cancel := context.WithTimeout(ctx, 4*time.Second) + defer cancel() + r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return 0, err + } + httpResponse, err := client.Do(r) + if err != nil { + return 0, err + } + c := make(map[uint64]float64) + reader := io.LimitReader(httpResponse.Body, 1<<14) + err = json.NewDecoder(reader).Decode(&c) + httpResponse.Body.Close() + if err != nil { + return 0, err + } + estimatedFeeRate, ok := c[nb] + if !ok { + return 0, errors.New("no fee rate for requested number of blocks") + } + return estimatedFeeRate, nil +} + // targetFeeRateWithFallback attempts to get a fresh fee rate for the target // number of confirmations, but falls back to the suggestion or fallbackFeeRate // via feeRateWithFallback. diff --git a/client/asset/interface.go b/client/asset/interface.go index c1d3706f06..8a28a6a7e8 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -229,6 +229,8 @@ type WalletConfig struct { // DataDir is a filesystem directory the the wallet may use for persistent // storage. DataDir string + // Network flags passed to the wallet representing which network to use. + Network dex.Network } // Wallet is a common interface to be implemented by cryptocurrency wallet diff --git a/client/core/core.go b/client/core/core.go index 941e41651b..6df9035af7 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -1914,6 +1914,7 @@ func (c *Core) loadWallet(dbWallet *db.Wallet) (*xcWallet, error) { } }, DataDir: c.assetDataDirectory(assetID), + Network: c.Network(), } walletCfg.Settings[asset.SpecialSettingActivelyUsed] =