status-go/services/wallet/history/balance.go

237 lines
6.1 KiB
Go

package history
import (
"context"
"errors"
"fmt"
"math/big"
"time"
"go.uber.org/zap"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/logutils"
)
const genesisTimestamp = 1438269988
// Specific time intervals for which balance history can be fetched
type TimeInterval int
const (
BalanceHistory7Days TimeInterval = iota + 1
BalanceHistory1Month
BalanceHistory6Months
BalanceHistory1Year
BalanceHistoryAllTime
)
const aDay = time.Duration(24) * time.Hour
var timeIntervalDuration = map[TimeInterval]time.Duration{
BalanceHistory7Days: time.Duration(7) * aDay,
BalanceHistory1Month: time.Duration(30) * aDay,
BalanceHistory6Months: time.Duration(6*30) * aDay,
BalanceHistory1Year: time.Duration(365) * aDay,
}
func TimeIntervalDurationSecs(timeInterval TimeInterval) uint64 {
return uint64(timeIntervalDuration[timeInterval].Seconds())
}
type DataPoint struct {
Balance *hexutil.Big
Timestamp uint64
BlockNumber *hexutil.Big
}
// String returns a string representation of the data point
func (d *DataPoint) String() string {
return fmt.Sprintf("timestamp: %d balance: %v block: %v", d.Timestamp, d.Balance.ToInt(), d.BlockNumber.ToInt())
}
type Balance struct {
db *BalanceDB
}
func NewBalance(db *BalanceDB) *Balance {
return &Balance{db}
}
// get returns the balance history for the given address from the given timestamp till now
func (b *Balance) get(ctx context.Context, chainID uint64, currency string, addresses []common.Address, fromTimestamp uint64) ([]*entry, error) {
logutils.ZapLogger().Debug("Getting balance history",
zap.Uint64("chainID", chainID),
zap.String("currency", currency),
zap.Stringers("address", addresses),
zap.Uint64("fromTimestamp", fromTimestamp),
)
cached, err := b.db.getNewerThan(&assetIdentity{chainID, addresses, currency}, fromTimestamp)
if err != nil {
return nil, err
}
return cached, nil
}
func (b *Balance) addEdgePoints(chainID uint64, currency string, addresses []common.Address, fromTimestamp, toTimestamp uint64, data []*entry) (res []*entry, err error) {
logutils.ZapLogger().Debug("Adding edge points",
zap.Uint64("chainID", chainID),
zap.String("currency", currency),
zap.Stringers("address", addresses),
zap.Uint64("fromTimestamp", fromTimestamp),
)
if len(addresses) == 0 {
return nil, errors.New("addresses must not be empty")
}
res = data
var firstEntry *entry
if len(data) > 0 {
firstEntry = data[0]
} else {
firstEntry = &entry{
chainID: chainID,
address: addresses[0],
tokenSymbol: currency,
timestamp: int64(fromTimestamp),
}
}
previous, err := b.db.getEntryPreviousTo(firstEntry)
if err != nil {
return nil, err
}
firstTimestamp, lastTimestamp := timestampBoundaries(fromTimestamp, toTimestamp, data)
if previous != nil {
previous.timestamp = int64(firstTimestamp) // We might need to use another minimal offset respecting the time interval
previous.block = nil
res = append([]*entry{previous}, res...)
} else {
// Add a zero point at the beginning to draw a line from
res = append([]*entry{
{
chainID: chainID,
address: addresses[0],
tokenSymbol: currency,
timestamp: int64(firstTimestamp),
balance: big.NewInt(0),
},
}, res...)
}
lastPoint := res[len(res)-1]
if lastPoint.timestamp < int64(lastTimestamp) {
// Add a last point to draw a line to
res = append(res, &entry{
chainID: chainID,
address: lastPoint.address,
tokenSymbol: currency,
timestamp: int64(lastTimestamp),
balance: lastPoint.balance,
})
}
return res, nil
}
func timestampBoundaries(fromTimestamp, toTimestamp uint64, data []*entry) (firstTimestamp, lastTimestamp uint64) {
firstTimestamp = fromTimestamp
if fromTimestamp == 0 {
if len(data) > 0 {
if data[0].timestamp == 0 {
panic("data[0].timestamp must never be 0")
}
firstTimestamp = uint64(data[0].timestamp) - 1
} else {
firstTimestamp = genesisTimestamp
}
}
if toTimestamp < firstTimestamp {
panic("toTimestamp < fromTimestamp")
}
lastTimestamp = toTimestamp
return firstTimestamp, lastTimestamp
}
func addPaddingPoints(currency string, addresses []common.Address, toTimestamp uint64, data []*entry, limit int) (res []*entry, err error) {
logutils.ZapLogger().Debug("addPaddingPoints start",
zap.String("currency", currency),
zap.Stringers("address", addresses),
zap.Int("len(data)", len(data)),
zap.Any("data", data),
zap.Int("limit", limit),
)
if len(data) < 2 { // Edge points must be added separately during the previous step
return nil, errors.New("slice is empty")
}
if limit <= len(data) {
return data, nil
}
fromTimestamp := uint64(data[0].timestamp)
delta := (toTimestamp - fromTimestamp) / uint64(limit-1)
res = make([]*entry, len(data))
copy(res, data)
var address common.Address
if len(addresses) > 0 {
address = addresses[0]
}
for i, j, index := 1, 0, 0; len(res) < limit; index++ {
// Add a last point to draw a line to. For some cases we might not need it,
// but when merging with points from other chains, we might get wrong balance if we don't have it.
paddingTimestamp := int64(fromTimestamp + delta*uint64(i))
if paddingTimestamp < data[j].timestamp {
// make a room for a new point
res = append(res[:index+1], res[index:]...)
// insert a new point
entry := &entry{
address: address,
tokenSymbol: currency,
timestamp: paddingTimestamp,
balance: data[j-1].balance, // take the previous balance
chainID: data[j-1].chainID,
}
res[index] = entry
logutils.ZapLogger().Debug("Added padding point",
zap.Stringer("entry", entry),
zap.Int64("timestamp", paddingTimestamp),
zap.Int("i", i),
zap.Int("j", j),
zap.Int("index", index),
)
i++
} else if paddingTimestamp >= data[j].timestamp {
logutils.ZapLogger().Debug("Kept real point",
zap.Any("entry", data[j]),
zap.Int64("timestamp", paddingTimestamp),
zap.Int("i", i),
zap.Int("j", j),
zap.Int("index", index),
)
j++
}
}
logutils.ZapLogger().Debug("addPaddingPoints end", zap.Int("len(res)", len(res)))
return res, nil
}