status-im/status-go

View on GitHub
services/wallet/history/balance.go

Summary

Maintainability
A
0 mins
Test Coverage
B
81%
package history

import (
    "context"
    "errors"
    "fmt"
    "math/big"
    "time"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/log"
)

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) {
    log.Debug("Getting balance history", "chainID", chainID, "currency", currency, "address", addresses, "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) {
    log.Debug("Adding edge points", "chainID", chainID, "currency", currency, "address", addresses, "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) {
    log.Debug("addPaddingPoints start", "currency", currency, "address", addresses, "len(data)", len(data), "data", data, "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

            log.Debug("Added padding point", "entry", entry, "timestamp", paddingTimestamp, "i", i, "j", j, "index", index)
            i++
        } else if paddingTimestamp >= data[j].timestamp {
            log.Debug("Kept real point", "entry", data[j], "timestamp", paddingTimestamp, "i", i, "j", j, "index", index)
            j++
        }
    }

    log.Debug("addPaddingPoints end", "len(res)", len(res))

    return res, nil
}