github.com/status-im/status-go@v1.1.0/services/wallet/history/balance.go (about)

     1  package history
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/big"
     8  	"time"
     9  
    10  	"github.com/ethereum/go-ethereum/common"
    11  	"github.com/ethereum/go-ethereum/common/hexutil"
    12  	"github.com/ethereum/go-ethereum/log"
    13  )
    14  
    15  const genesisTimestamp = 1438269988
    16  
    17  // Specific time intervals for which balance history can be fetched
    18  type TimeInterval int
    19  
    20  const (
    21  	BalanceHistory7Days TimeInterval = iota + 1
    22  	BalanceHistory1Month
    23  	BalanceHistory6Months
    24  	BalanceHistory1Year
    25  	BalanceHistoryAllTime
    26  )
    27  
    28  const aDay = time.Duration(24) * time.Hour
    29  
    30  var timeIntervalDuration = map[TimeInterval]time.Duration{
    31  	BalanceHistory7Days:   time.Duration(7) * aDay,
    32  	BalanceHistory1Month:  time.Duration(30) * aDay,
    33  	BalanceHistory6Months: time.Duration(6*30) * aDay,
    34  	BalanceHistory1Year:   time.Duration(365) * aDay,
    35  }
    36  
    37  func TimeIntervalDurationSecs(timeInterval TimeInterval) uint64 {
    38  	return uint64(timeIntervalDuration[timeInterval].Seconds())
    39  }
    40  
    41  type DataPoint struct {
    42  	Balance     *hexutil.Big
    43  	Timestamp   uint64
    44  	BlockNumber *hexutil.Big
    45  }
    46  
    47  // String returns a string representation of the data point
    48  func (d *DataPoint) String() string {
    49  	return fmt.Sprintf("timestamp: %d balance: %v block: %v", d.Timestamp, d.Balance.ToInt(), d.BlockNumber.ToInt())
    50  }
    51  
    52  type Balance struct {
    53  	db *BalanceDB
    54  }
    55  
    56  func NewBalance(db *BalanceDB) *Balance {
    57  	return &Balance{db}
    58  }
    59  
    60  // get returns the balance history for the given address from the given timestamp till now
    61  func (b *Balance) get(ctx context.Context, chainID uint64, currency string, addresses []common.Address, fromTimestamp uint64) ([]*entry, error) {
    62  	log.Debug("Getting balance history", "chainID", chainID, "currency", currency, "address", addresses, "fromTimestamp", fromTimestamp)
    63  
    64  	cached, err := b.db.getNewerThan(&assetIdentity{chainID, addresses, currency}, fromTimestamp)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	return cached, nil
    70  }
    71  
    72  func (b *Balance) addEdgePoints(chainID uint64, currency string, addresses []common.Address, fromTimestamp, toTimestamp uint64, data []*entry) (res []*entry, err error) {
    73  	log.Debug("Adding edge points", "chainID", chainID, "currency", currency, "address", addresses, "fromTimestamp", fromTimestamp)
    74  
    75  	if len(addresses) == 0 {
    76  		return nil, errors.New("addresses must not be empty")
    77  	}
    78  
    79  	res = data
    80  
    81  	var firstEntry *entry
    82  
    83  	if len(data) > 0 {
    84  		firstEntry = data[0]
    85  	} else {
    86  		firstEntry = &entry{
    87  			chainID:     chainID,
    88  			address:     addresses[0],
    89  			tokenSymbol: currency,
    90  			timestamp:   int64(fromTimestamp),
    91  		}
    92  	}
    93  
    94  	previous, err := b.db.getEntryPreviousTo(firstEntry)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	firstTimestamp, lastTimestamp := timestampBoundaries(fromTimestamp, toTimestamp, data)
   100  
   101  	if previous != nil {
   102  		previous.timestamp = int64(firstTimestamp) // We might need to use another minimal offset respecting the time interval
   103  		previous.block = nil
   104  		res = append([]*entry{previous}, res...)
   105  	} else {
   106  		// Add a zero point at the beginning to draw a line from
   107  		res = append([]*entry{
   108  			{
   109  				chainID:     chainID,
   110  				address:     addresses[0],
   111  				tokenSymbol: currency,
   112  				timestamp:   int64(firstTimestamp),
   113  				balance:     big.NewInt(0),
   114  			},
   115  		}, res...)
   116  	}
   117  
   118  	lastPoint := res[len(res)-1]
   119  	if lastPoint.timestamp < int64(lastTimestamp) {
   120  		// Add a last point to draw a line to
   121  		res = append(res, &entry{
   122  			chainID:     chainID,
   123  			address:     lastPoint.address,
   124  			tokenSymbol: currency,
   125  			timestamp:   int64(lastTimestamp),
   126  			balance:     lastPoint.balance,
   127  		})
   128  	}
   129  
   130  	return res, nil
   131  }
   132  
   133  func timestampBoundaries(fromTimestamp, toTimestamp uint64, data []*entry) (firstTimestamp, lastTimestamp uint64) {
   134  	firstTimestamp = fromTimestamp
   135  	if fromTimestamp == 0 {
   136  		if len(data) > 0 {
   137  			if data[0].timestamp == 0 {
   138  				panic("data[0].timestamp must never be 0")
   139  			}
   140  			firstTimestamp = uint64(data[0].timestamp) - 1
   141  		} else {
   142  			firstTimestamp = genesisTimestamp
   143  		}
   144  	}
   145  
   146  	if toTimestamp < firstTimestamp {
   147  		panic("toTimestamp < fromTimestamp")
   148  	}
   149  
   150  	lastTimestamp = toTimestamp
   151  
   152  	return firstTimestamp, lastTimestamp
   153  }
   154  
   155  func addPaddingPoints(currency string, addresses []common.Address, toTimestamp uint64, data []*entry, limit int) (res []*entry, err error) {
   156  	log.Debug("addPaddingPoints start", "currency", currency, "address", addresses, "len(data)", len(data), "data", data, "limit", limit)
   157  
   158  	if len(data) < 2 { // Edge points must be added separately during the previous step
   159  		return nil, errors.New("slice is empty")
   160  	}
   161  
   162  	if limit <= len(data) {
   163  		return data, nil
   164  	}
   165  
   166  	fromTimestamp := uint64(data[0].timestamp)
   167  	delta := (toTimestamp - fromTimestamp) / uint64(limit-1)
   168  
   169  	res = make([]*entry, len(data))
   170  	copy(res, data)
   171  
   172  	var address common.Address
   173  	if len(addresses) > 0 {
   174  		address = addresses[0]
   175  	}
   176  
   177  	for i, j, index := 1, 0, 0; len(res) < limit; index++ {
   178  		// Add a last point to draw a line to. For some cases we might not need it,
   179  		// but when merging with points from other chains, we might get wrong balance if we don't have it.
   180  		paddingTimestamp := int64(fromTimestamp + delta*uint64(i))
   181  
   182  		if paddingTimestamp < data[j].timestamp {
   183  			// make a room for a new point
   184  			res = append(res[:index+1], res[index:]...)
   185  			// insert a new point
   186  			entry := &entry{
   187  				address:     address,
   188  				tokenSymbol: currency,
   189  				timestamp:   paddingTimestamp,
   190  				balance:     data[j-1].balance, // take the previous balance
   191  				chainID:     data[j-1].chainID,
   192  			}
   193  			res[index] = entry
   194  
   195  			log.Debug("Added padding point", "entry", entry, "timestamp", paddingTimestamp, "i", i, "j", j, "index", index)
   196  			i++
   197  		} else if paddingTimestamp >= data[j].timestamp {
   198  			log.Debug("Kept real point", "entry", data[j], "timestamp", paddingTimestamp, "i", i, "j", j, "index", index)
   199  			j++
   200  		}
   201  	}
   202  
   203  	log.Debug("addPaddingPoints end", "len(res)", len(res))
   204  
   205  	return res, nil
   206  }