Files
blockbook/common/utils.go
Jiří Musil b14641d979 feat: support new alternative_estimate_fee option - mempoolspacemedian (#1233)
* feat: support new alternative_estimate_fee option - mempoolspacemedian

* chore: introduce a fallback median fee rate in case it comes zero from mempool space response

* chore: add and test util function for rounding to significant digits

* chore: use rounding of medianFee to 3 significant digits

* chore: make the new alternative_estimate_fee be configurable, change its name from Median to Block

* chore: return 1000 sats/kB fee for MempoolSpaceBlock estimation method when block not identified

* chore: make the fallback fee rate configurable, improve tests, improve function names
2025-05-07 10:36:46 +02:00

109 lines
2.9 KiB
Go

package common
import (
"encoding/json"
"io"
"math"
"runtime/debug"
"time"
"github.com/golang/glog"
"github.com/juju/errors"
)
// TickAndDebounce calls function f on trigger channel or with tickTime period (whatever is sooner) with debounce
func TickAndDebounce(tickTime time.Duration, debounceTime time.Duration, trigger chan struct{}, f func()) {
timer := time.NewTimer(tickTime)
var firstDebounce time.Time
Loop:
for {
select {
case _, ok := <-trigger:
if !timer.Stop() {
<-timer.C
}
// exit loop on closed input channel
if !ok {
break Loop
}
if firstDebounce.IsZero() {
firstDebounce = time.Now()
}
// debounce for up to debounceTime period
// afterwards execute immediately
if firstDebounce.Add(debounceTime).After(time.Now()) {
timer.Reset(debounceTime)
} else {
timer.Reset(0)
}
case <-timer.C:
// do the action, if not in shutdown, then start the loop again
if !IsInShutdown() {
f()
}
timer.Reset(tickTime)
firstDebounce = time.Time{}
}
}
}
// SafeDecodeResponseFromReader reads from io.ReadCloser safely, with recovery from panic
func SafeDecodeResponseFromReader(body io.ReadCloser, res interface{}) (err error) {
var data []byte
defer func() {
if r := recover(); r != nil {
glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data))
debug.PrintStack()
if len(data) > 0 && len(data) < 2048 {
err = errors.Errorf("Error: %v", string(data))
} else {
err = errors.New("Internal error")
}
}
}()
data, err = io.ReadAll(body)
if err != nil {
return err
}
return json.Unmarshal(data, &res)
}
// RoundToSignificantDigits rounds a float64 number `n` to the specified number of significant figures `digits`.
// For example, RoundToSignificantDigits(1234, 3) returns 1230
//
// This function works by shifting the number's decimal point to make the desired significant figures
// into whole numbers, rounding, and then shifting back.
//
// Example for n = 1234, digits = 3:
//
// log10(1234) ≈ 3.09 → ceil = 4
// power = 3 - 4 = -1
// magnitude = 10^-1 = 0.1
// n * magnitude = 1234 * 0.1 = 123.4
// round(123.4) = 123
// 123 / 0.1 = 1230
//
// Returns the number rounded to the desired number of significant figures.
func RoundToSignificantDigits(n float64, digits int) float64 {
if n == 0 {
return 0
}
// Step 1: Compute how many digits are before the decimal point.
// For 1234 → log10(1234) ≈ 3.09 → ceil = 4
d := math.Ceil(math.Log10(math.Abs(n)))
// Step 2: Calculate how much we need to shift the number to bring
// the significant digits into the integer part.
// For digits=3 and d=4 → power = -1
power := digits - int(d)
// Step 3: Compute 10^power to scale the number
// 10^-1 = 0.1
magnitude := math.Pow(10, float64(power))
// Step 4: Scale, round, and scale back
// 1234 * 0.1 = 123.4 → round = 123 → 123 / 0.1 = 1230
return math.Round(n*magnitude) / magnitude
}