Skip to content

Commit b89b615

Browse files
committed
Merge branch 'mempool-space'
2 parents 7deabeb + bd52d93 commit b89b615

File tree

9 files changed

+155
-60
lines changed

9 files changed

+155
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Support pasting different localized number formats, i.e. dot and comma separated amounts
99
- Fix BitBoxApp crash on GrapheneOS and other phones without Google Play Services when scanning QR codes.
1010
- Add DMG installer for macOS
11+
- Use mempool.space as preferred fee estimation source for BTC
1112

1213
## 4.42.0
1314
- Preselect backup when there's only one backup available

backend/accounts/feetarget.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package accounts
1616

1717
import (
1818
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
19+
"github.com/btcsuite/btcd/btcutil"
1920
)
2021

2122
// FeeTarget interface has priority codes.
@@ -39,6 +40,10 @@ func NewFeeTargetCode(code string) (FeeTargetCode, error) {
3940
case string(FeeTargetCodeNormal):
4041
case string(FeeTargetCodeHigh):
4142
case string(FeeTargetCodeCustom):
43+
case string(FeeTargetCodeMempoolFastest):
44+
case string(FeeTargetCodeMempoolHalfHour):
45+
case string(FeeTargetCodeMempoolHour):
46+
case string(FeeTargetCodeMempoolEconomy):
4247
default:
4348
return "", errp.WithStack(errp.Newf("Unrecognized fee target code %s", code))
4449
}
@@ -58,10 +63,51 @@ const (
5863
// FeeTargetCodeHigh is the high priority fee target.
5964
FeeTargetCodeHigh FeeTargetCode = "high"
6065

66+
// FeeTargetCodeMempoolFastest is the mempool highest priority fee target.
67+
FeeTargetCodeMempoolFastest FeeTargetCode = "mFastest"
68+
69+
// FeeTargetCodeMempoolHalfHour is the mempool half hour fee target.
70+
FeeTargetCodeMempoolHalfHour FeeTargetCode = "mHalfHour"
71+
72+
// FeeTargetCodeMempoolHour is the mempool hour fee target.
73+
FeeTargetCodeMempoolHour FeeTargetCode = "mHour"
74+
75+
// FeeTargetCodeMempoolEconomy is the mempool economy fee target.
76+
FeeTargetCodeMempoolEconomy FeeTargetCode = "mEconomy"
77+
6178
// FeeTargetCodeCustom means that the actual feerate is supplied separately instead of being
6279
// estimated automatically.
6380
FeeTargetCodeCustom FeeTargetCode = "custom"
6481

82+
// DefaultMempoolFeeTarget is the default fee target for mempool fees.
83+
DefaultMempoolFeeTarget = FeeTargetCodeMempoolHalfHour
84+
6585
// DefaultFeeTarget is the default fee target.
6686
DefaultFeeTarget = FeeTargetCodeNormal
6787
)
88+
89+
// MempoolSpaceFees contains mempool.space recommended fees API response
90+
// (https://mempool.space/docs/api/rest#get-recommended-fees)
91+
type MempoolSpaceFees struct {
92+
FastestFee int64 `json:"fastestFee"`
93+
HalfHourFee int64 `json:"halfHourFee"`
94+
HourFee int64 `json:"hourFee"`
95+
EconomyFee int64 `json:"economyFee"`
96+
MinimumFee int64 `json:"minimumFee"`
97+
}
98+
99+
// GetFeeRate returns the btcutil.Amount of the fee for the given FeeTargetCode.
100+
func (fees MempoolSpaceFees) GetFeeRate(code FeeTargetCode) btcutil.Amount {
101+
var feeRatePerByte int64
102+
switch code {
103+
case FeeTargetCodeMempoolFastest:
104+
feeRatePerByte = fees.FastestFee
105+
case FeeTargetCodeMempoolHalfHour:
106+
feeRatePerByte = fees.HalfHourFee
107+
case FeeTargetCodeMempoolHour:
108+
feeRatePerByte = fees.HourFee
109+
case FeeTargetCodeMempoolEconomy:
110+
feeRatePerByte = fees.EconomyFee
111+
}
112+
return btcutil.Amount(feeRatePerByte * 1000)
113+
}

backend/accounts/types/event.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,4 @@ const (
3131

3232
// EventHeadersSynced is fired when the headers finished syncing.
3333
EventHeadersSynced Event = "headersSynced"
34-
35-
// EventFeeTargetsChanged is fired when the fee targets change.
36-
EventFeeTargetsChanged Event = "feeTargetsChanged"
3734
)

backend/backend.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
240240
}
241241
log.Infof("backend config: %+v", config.AppConfig().Backend)
242242
log.Infof("frontend config: %+v", config.AppConfig().Frontend)
243+
backendProxy := socksproxy.NewSocksProxy(
244+
config.AppConfig().Backend.Proxy.UseProxy,
245+
config.AppConfig().Backend.Proxy.ProxyAddress,
246+
)
247+
hclient, err := backendProxy.GetHTTPClient()
248+
if err != nil {
249+
return nil, err
250+
}
251+
243252
backend := &Backend{
244253
arguments: arguments,
245254
environment: environment,
@@ -252,7 +261,7 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
252261
aopp: AOPP{State: aoppStateInactive},
253262

254263
makeBtcAccount: func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, log *logrus.Entry) accounts.Interface {
255-
return btc.NewAccount(config, coin, gapLimits, log)
264+
return btc.NewAccount(config, coin, gapLimits, log, hclient)
256265
},
257266
makeEthAccount: func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {
258267
return eth.NewAccount(config, coin, httpClient, log)
@@ -265,14 +274,7 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
265274
return nil, err
266275
}
267276
backend.notifier = notifier
268-
backend.socksProxy = socksproxy.NewSocksProxy(
269-
backend.config.AppConfig().Backend.Proxy.UseProxy,
270-
backend.config.AppConfig().Backend.Proxy.ProxyAddress,
271-
)
272-
hclient, err := backend.socksProxy.GetHTTPClient()
273-
if err != nil {
274-
return nil, err
275-
}
277+
backend.socksProxy = backendProxy
276278
backend.httpClient = hclient
277279
backend.etherScanHTTPClient = ratelimit.FromTransport(hclient.Transport, etherscan.CallInterval)
278280

backend/coins/btc/account.go

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package btc
1818
import (
1919
"encoding/base64"
2020
"fmt"
21+
"net/http"
2122
"os"
2223
"path"
2324
"sort"
@@ -36,6 +37,7 @@ import (
3637
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
3738
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/ltc"
3839
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
40+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/util"
3941
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
4042
"github.com/BitBoxSwiss/bitbox-wallet-app/util/locker"
4143
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable"
@@ -56,6 +58,10 @@ const (
5658
// maxGapLimit limits the maximum gap limit that can be used. It is an arbitrary number with the
5759
// goal that the scanning will stop in a reasonable amount of time.
5860
maxGapLimit = 2000
61+
62+
// mempoolSpaceMirror is Shift server that mirrors "https://mempool.space/api/v1/fees/recommended"
63+
// rest call.
64+
mempoolSpaceMirror = "https://fees1.shiftcrypto.io"
5965
)
6066

6167
type subaccount struct {
@@ -101,8 +107,6 @@ type Account struct {
101107
activeTxProposal *maketx.TxProposal
102108
activeTxProposalLock locker.Locker
103109

104-
feeTargets []*FeeTarget
105-
feeTargetsLock locker.Locker
106110
// Access this only via getMinRelayFeeRate(). sat/kB.
107111
minRelayFeeRate *btcutil.Amount
108112
minRelayFeeRateLock locker.Locker
@@ -116,6 +120,8 @@ type Account struct {
116120
closed bool
117121

118122
log *logrus.Entry
123+
124+
httpClient *http.Client
119125
}
120126

121127
// NewAccount creates a new account.
@@ -126,6 +132,7 @@ func NewAccount(
126132
coin *Coin,
127133
forceGapLimits *types.GapLimits,
128134
log *logrus.Entry,
135+
httpClient *http.Client,
129136
) *Account {
130137
log = log.WithField("group", "btc").
131138
WithFields(logrus.Fields{"coin": coin.String(), "code": config.Config.Code, "name": config.Config.Name})
@@ -137,14 +144,8 @@ func NewAccount(
137144
dbSubfolder: "", // set in Initialize()
138145
forceGapLimits: forceGapLimits,
139146

140-
// feeTargets must be sorted by ascending priority.
141-
feeTargets: []*FeeTarget{
142-
{blocks: 24, code: accounts.FeeTargetCodeEconomy},
143-
{blocks: 12, code: accounts.FeeTargetCodeLow},
144-
{blocks: 6, code: accounts.FeeTargetCodeNormal},
145-
{blocks: 2, code: accounts.FeeTargetCodeHigh},
146-
},
147-
log: log,
147+
log: log,
148+
httpClient: httpClient,
148149
}
149150
return account
150151
}
@@ -430,8 +431,6 @@ func (account *Account) onNewHeader(header *electrumTypes.Header) {
430431
return
431432
}
432433
account.log.WithField("block-height", header.Height).Debug("Received new header")
433-
// Fee estimates change with each block.
434-
account.updateFeeTargets()
435434
}
436435

437436
// FatalError returns true if the account had a fatal error.
@@ -476,26 +475,68 @@ func (account *Account) Notifier() accounts.Notifier {
476475
return account.notifier
477476
}
478477

479-
func (account *Account) updateFeeTargets() {
480-
defer account.feeTargetsLock.Lock()()
478+
// feeTargets fetches the available fees. For mainnet BTC it uses mempool.space estimation.
479+
//
480+
// For the other coins or in case mempool.space is not available it fallbacks on Bitcoin Core.
481+
// The minimum relay fee is used as a last resource fallback in case also Bitcoin Core is
482+
// unavailable.
483+
func (account *Account) feeTargets() []*FeeTarget {
484+
// for mainnet BTC we fetch mempool.space fees, as they should be more reliable.
485+
var mempoolFees *accounts.MempoolSpaceFees
486+
if account.coin.Code() == coin.CodeBTC {
487+
mempoolFees = &accounts.MempoolSpaceFees{}
488+
_, err := util.APIGet(account.httpClient, mempoolSpaceMirror, "", 1000, mempoolFees)
489+
if err != nil {
490+
mempoolFees = nil
491+
account.log.WithError(err).Errorf("Fetching fees from %s failed", mempoolSpaceMirror)
492+
}
493+
}
494+
495+
// feeTargets must be sorted by ascending priority.
496+
var feeTargets []*FeeTarget
497+
if mempoolFees != nil {
498+
feeTargets = []*FeeTarget{
499+
{blocks: 12, code: accounts.FeeTargetCodeMempoolEconomy},
500+
{blocks: 3, code: accounts.FeeTargetCodeMempoolHour},
501+
{blocks: 2, code: accounts.FeeTargetCodeMempoolHalfHour},
502+
{blocks: 1, code: accounts.FeeTargetCodeMempoolFastest},
503+
}
504+
} else {
505+
feeTargets = []*FeeTarget{
506+
{blocks: 24, code: accounts.FeeTargetCodeEconomy},
507+
{blocks: 12, code: accounts.FeeTargetCodeLow},
508+
{blocks: 6, code: accounts.FeeTargetCodeNormal},
509+
{blocks: 2, code: accounts.FeeTargetCodeHigh},
510+
}
511+
}
512+
481513
var minRelayFeeRate *btcutil.Amount
482514
minRelayFeeRateVal, err := account.getMinRelayFeeRate()
483515
if err == nil {
484516
minRelayFeeRate = &minRelayFeeRateVal
485517
}
486-
for _, feeTarget := range account.feeTargets {
487-
feeRatePerKb, err := account.coin.Blockchain().EstimateFee(feeTarget.blocks)
488-
if err != nil {
489-
if account.coin.Code() != coin.CodeTLTC {
490-
account.log.WithField("fee-target", feeTarget.blocks).
491-
Warning("Fee could not be estimated. Taking the minimum relay fee instead")
492-
}
493-
if minRelayFeeRate == nil {
494-
account.log.WithField("fee-target", feeTarget.blocks).
495-
Warning("Minimum relay fee could not be determined")
496-
continue
518+
519+
for _, feeTarget := range feeTargets {
520+
var feeRatePerKb btcutil.Amount
521+
522+
if mempoolFees != nil {
523+
feeRatePerKb = mempoolFees.GetFeeRate(feeTarget.code)
524+
} else {
525+
// If mempool.space fees are not available, we fallback on Bitcoin Core estimation.
526+
// If even that one is not available, we just offer the min relay fee.
527+
feeRatePerKb, err = account.coin.Blockchain().EstimateFee(feeTarget.blocks)
528+
if err != nil {
529+
if account.coin.Code() != coin.CodeTLTC {
530+
account.log.WithField("fee-target", feeTarget.blocks).
531+
Warning("Fee could not be estimated. Taking the minimum relay fee instead")
532+
}
533+
if minRelayFeeRate == nil {
534+
account.log.WithField("fee-target", feeTarget.blocks).
535+
Warning("Minimum relay fee could not be determined")
536+
continue
537+
}
538+
feeRatePerKb = *minRelayFeeRate
497539
}
498-
feeRatePerKb = *minRelayFeeRate
499540
}
500541
// If the minrelayfee is available the estimated fee rate is smaller than the minrelayfee,
501542
// we use the minrelayfee instead. If the minrelayfee is unknown, we leave the fee
@@ -506,42 +547,36 @@ func (account *Account) updateFeeTargets() {
506547
feeTarget.feeRatePerKb = &feeRatePerKb
507548
account.log.WithFields(logrus.Fields{"blocks": feeTarget.blocks,
508549
"fee-rate-per-kb": feeRatePerKb}).Debug("Fee estimate per kb")
509-
account.Config().OnEvent(accountsTypes.EventFeeTargetsChanged)
510550
}
551+
552+
return feeTargets
511553
}
512554

513555
// FeeTargets returns the fee targets and the default fee target.
514556
func (account *Account) FeeTargets() ([]accounts.FeeTarget, accounts.FeeTargetCode) {
515-
defer account.feeTargetsLock.RLock()()
516-
// Return only fee targets with a valid fee rate (drop if fee could not be estimated). Also
517-
// remove all duplicate fee rates.
557+
// Return only fee targets with a valid fee rate (drop if fee could not be estimated).
558+
fetchedFeeTargets := account.feeTargets()
518559
feeTargets := []accounts.FeeTarget{}
519-
defaultAvailable := false
520-
outer:
521-
for i := len(account.feeTargets) - 1; i >= 0; i-- {
522-
feeTarget := account.feeTargets[i]
560+
defaultFee := accounts.FeeTargetCodeCustom
561+
562+
for _, feeTarget := range fetchedFeeTargets {
523563
if feeTarget.feeRatePerKb == nil {
524564
continue
525565
}
526-
for j := i - 1; j >= 0; j-- {
527-
checkFeeTarget := account.feeTargets[j]
528-
if checkFeeTarget.feeRatePerKb != nil && *checkFeeTarget.feeRatePerKb == *feeTarget.feeRatePerKb {
529-
continue outer
530-
}
531-
}
532-
if feeTarget.code == accounts.DefaultFeeTarget {
533-
defaultAvailable = true
566+
567+
switch feeTarget.code {
568+
case accounts.DefaultFeeTarget:
569+
fallthrough
570+
case accounts.DefaultMempoolFeeTarget:
571+
defaultFee = feeTarget.code
534572
}
535573
feeTargets = append(feeTargets, feeTarget)
536574
}
537575
// If the default fee level was dropped, use the cheapest.
538576
// If no fee targets are available, use custom (the user can manually enter a fee rate).
539-
defaultFee := accounts.DefaultFeeTarget
540-
if !defaultAvailable {
577+
if defaultFee == accounts.FeeTargetCodeCustom {
541578
if len(feeTargets) != 0 {
542579
defaultFee = feeTargets[0].Code()
543-
} else {
544-
defaultFee = accounts.FeeTargetCodeCustom
545580
}
546581
}
547582
return feeTargets, defaultFee

backend/coins/btc/account_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func mockAccount(t *testing.T, accountConfig *config.Account) *btc.Account {
103103
},
104104
coin, nil,
105105
logging.Get().WithGroup("account_test"),
106+
nil,
106107
)
107108
}
108109

backend/coins/btc/transaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (account *Account) getFeePerKb(args *accounts.TxProposalArgs) (btcutil.Amou
5959
return feePerKb, nil
6060
}
6161
var feeTarget *FeeTarget
62-
for _, target := range account.feeTargets {
62+
for _, target := range account.feeTargets() {
6363
if target.code == args.FeeTargetCode {
6464
feeTarget = target
6565
break

backend/util/util.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
package util
1616

1717
import (
18+
"context"
1819
"encoding/json"
1920
"fmt"
2021
"io"
2122
"net/http"
23+
"time"
2224

2325
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
2426
)
@@ -32,7 +34,10 @@ import (
3234
// - `result` object that should be used to unmarshal the response body
3335
// Returns the error code (if available) and possibly an error.
3436
func APIGet(httpClient *http.Client, endpoint string, apiKey string, maxSize int64, result interface{}) (int, error) {
35-
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
37+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
38+
defer cancel()
39+
40+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
3641
if err != nil {
3742
return 0, errp.WithStack(err)
3843
}

0 commit comments

Comments
 (0)