724 lines
26 KiB
Go
724 lines
26 KiB
Go
|
package history
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"math"
|
||
|
"math/big"
|
||
|
"sort"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/ethereum/go-ethereum"
|
||
|
"github.com/ethereum/go-ethereum/common"
|
||
|
"github.com/ethereum/go-ethereum/core/types"
|
||
|
|
||
|
"github.com/status-im/status-go/appdatabase"
|
||
|
"github.com/status-im/status-go/sqlite"
|
||
|
|
||
|
"github.com/stretchr/testify/require"
|
||
|
)
|
||
|
|
||
|
func setupBalanceTest(t *testing.T) (*Balance, func()) {
|
||
|
db, err := appdatabase.InitializeDB(":memory:", "wallet-history-balance-tests", sqlite.ReducedKDFIterationsNumber)
|
||
|
require.NoError(t, err)
|
||
|
return NewBalance(NewBalanceDB(db)), func() {
|
||
|
require.NoError(t, db.Close())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type requestedBlock struct {
|
||
|
time uint64
|
||
|
headerInfoRequests int
|
||
|
balanceRequests int
|
||
|
}
|
||
|
|
||
|
// chainClientTestSource is a test implementation of the DataSource interface
|
||
|
// It generates dummy consecutive blocks of data and stores them for validation
|
||
|
type chainClientTestSource struct {
|
||
|
t *testing.T
|
||
|
firstTimeRequest int64
|
||
|
requestedBlocks map[int64]*requestedBlock // map of block number to block data
|
||
|
lastBlockTimestamp int64
|
||
|
firstBlockTimestamp int64
|
||
|
headerByNumberFn func(ctx context.Context, number *big.Int) (*types.Header, error)
|
||
|
balanceAtFn func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
|
||
|
mockTime int64
|
||
|
timeAtMock int64
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
testTimeLayout = "2006-01-02 15:04:05 Z07:00"
|
||
|
testTime = "2022-12-15 12:01:10 +02:00"
|
||
|
oneYear = 365 * 24 * time.Hour
|
||
|
)
|
||
|
|
||
|
func getTestTime(t *testing.T) time.Time {
|
||
|
testTime, err := time.Parse(testTimeLayout, testTime)
|
||
|
require.NoError(t, err)
|
||
|
return testTime.UTC()
|
||
|
}
|
||
|
|
||
|
func newTestSource(t *testing.T, availableYears float64) *chainClientTestSource {
|
||
|
return newTestSourceWithCurrentTime(t, availableYears, getTestTime(t).Unix())
|
||
|
}
|
||
|
|
||
|
func newTestSourceWithCurrentTime(t *testing.T, availableYears float64, currentTime int64) *chainClientTestSource {
|
||
|
newInst := &chainClientTestSource{
|
||
|
t: t,
|
||
|
requestedBlocks: make(map[int64]*requestedBlock),
|
||
|
lastBlockTimestamp: currentTime,
|
||
|
firstBlockTimestamp: currentTime - int64(availableYears*oneYear.Seconds()),
|
||
|
mockTime: currentTime,
|
||
|
timeAtMock: time.Now().UTC().Unix(),
|
||
|
}
|
||
|
newInst.headerByNumberFn = newInst.HeaderByNumberMock
|
||
|
newInst.balanceAtFn = newInst.BalanceAtMock
|
||
|
return newInst
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
averageBlockTimeSeconds = 12.1
|
||
|
)
|
||
|
|
||
|
func (src *chainClientTestSource) setCurrentTime(newTime int64) {
|
||
|
src.mockTime = newTime
|
||
|
src.lastBlockTimestamp = newTime
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) resetStats() {
|
||
|
src.requestedBlocks = make(map[int64]*requestedBlock)
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) availableYears() float64 {
|
||
|
return float64(src.TimeNow()-src.firstBlockTimestamp) / oneYear.Seconds()
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) blocksCount() int64 {
|
||
|
return int64(math.Round(float64(src.TimeNow()-src.firstBlockTimestamp) / averageBlockTimeSeconds))
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) blockNumberToTimestamp(number int64) int64 {
|
||
|
return src.firstBlockTimestamp + int64(float64(number)*averageBlockTimeSeconds)
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) generateBlockInfo(blockNumber int64, time uint64) *types.Header {
|
||
|
return &types.Header{
|
||
|
Number: big.NewInt(blockNumber),
|
||
|
Time: time,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
|
||
|
return src.headerByNumberFn(ctx, number)
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) HeaderByNumberMock(ctx context.Context, number *big.Int) (*types.Header, error) {
|
||
|
var blockNo int64
|
||
|
if number == nil {
|
||
|
// Last block was requested
|
||
|
blockNo = src.blocksCount()
|
||
|
} else if number.Cmp(big.NewInt(src.blocksCount())) > 0 {
|
||
|
return nil, ethereum.NotFound
|
||
|
} else {
|
||
|
require.Greater(src.t, number.Int64(), int64(0))
|
||
|
blockNo = number.Int64()
|
||
|
}
|
||
|
timestamp := src.blockNumberToTimestamp(blockNo)
|
||
|
|
||
|
if _, contains := src.requestedBlocks[blockNo]; contains {
|
||
|
src.requestedBlocks[blockNo].headerInfoRequests++
|
||
|
} else {
|
||
|
src.requestedBlocks[blockNo] = &requestedBlock{
|
||
|
time: uint64(timestamp),
|
||
|
headerInfoRequests: 1,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return src.generateBlockInfo(blockNo, uint64(timestamp)), nil
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
return src.balanceAtFn(ctx, account, blockNumber)
|
||
|
}
|
||
|
|
||
|
func weiInEth() *big.Int {
|
||
|
res, _ := new(big.Int).SetString("1000000000000000000", 0)
|
||
|
return res
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) BalanceAtMock(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
var blockNo int64
|
||
|
if blockNumber == nil {
|
||
|
// Last block was requested
|
||
|
blockNo = src.blocksCount()
|
||
|
} else if blockNumber.Cmp(big.NewInt(src.blocksCount())) > 0 {
|
||
|
return nil, ethereum.NotFound
|
||
|
} else {
|
||
|
require.Greater(src.t, blockNumber.Int64(), int64(0))
|
||
|
blockNo = blockNumber.Int64()
|
||
|
}
|
||
|
|
||
|
if _, contains := src.requestedBlocks[blockNo]; contains {
|
||
|
src.requestedBlocks[blockNo].balanceRequests++
|
||
|
} else {
|
||
|
src.requestedBlocks[blockNo] = &requestedBlock{
|
||
|
time: uint64(src.blockNumberToTimestamp(blockNo)),
|
||
|
balanceRequests: 1,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return new(big.Int).Mul(big.NewInt(blockNo), weiInEth()), nil
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) ChainID() uint64 {
|
||
|
return 777
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) Currency() string {
|
||
|
return "eth"
|
||
|
}
|
||
|
|
||
|
func (src *chainClientTestSource) TimeNow() int64 {
|
||
|
if src.firstTimeRequest == 0 {
|
||
|
src.firstTimeRequest = time.Now().UTC().Unix()
|
||
|
}
|
||
|
return src.mockTime + (time.Now().UTC().Unix() - src.firstTimeRequest)
|
||
|
}
|
||
|
|
||
|
// extractTestData returns reqBlkNos sorted in ascending order
|
||
|
func extractTestData(dataSource *chainClientTestSource) (reqBlkNos []int64, infoRequests map[int64]int, balanceRequests map[int64]int) {
|
||
|
reqBlkNos = make([]int64, 0, len(dataSource.requestedBlocks))
|
||
|
for blockNo := range dataSource.requestedBlocks {
|
||
|
reqBlkNos = append(reqBlkNos, blockNo)
|
||
|
}
|
||
|
sort.Slice(reqBlkNos, func(i, j int) bool {
|
||
|
return reqBlkNos[i] < reqBlkNos[j]
|
||
|
})
|
||
|
|
||
|
infoRequests = make(map[int64]int)
|
||
|
balanceRequests = make(map[int64]int, len(reqBlkNos))
|
||
|
for i := 0; i < len(reqBlkNos); i++ {
|
||
|
n := reqBlkNos[i]
|
||
|
rB := dataSource.requestedBlocks[n]
|
||
|
|
||
|
if rB.headerInfoRequests > 0 {
|
||
|
infoRequests[n] = rB.headerInfoRequests
|
||
|
}
|
||
|
if rB.balanceRequests > 0 {
|
||
|
balanceRequests[n] = rB.balanceRequests
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func minimumExpectedDataPoints(interval TimeInterval) int {
|
||
|
return int(math.Ceil(float64(timeIntervalDuration[interval]) / float64(strideDuration(interval))))
|
||
|
}
|
||
|
|
||
|
func getTimeError(dataSource *chainClientTestSource, data []*DataPoint, interval TimeInterval) int64 {
|
||
|
timeRange := int64(data[len(data)-1].Timestamp - data[0].Timestamp)
|
||
|
var expectedDuration int64
|
||
|
if interval != BalanceHistoryAllTime {
|
||
|
expectedDuration = int64(timeIntervalDuration[interval].Seconds())
|
||
|
} else {
|
||
|
expectedDuration = int64((time.Duration(dataSource.availableYears()) * oneYear).Seconds())
|
||
|
}
|
||
|
return timeRange - expectedDuration
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryGetWithoutFetch(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
|
||
|
testData := []struct {
|
||
|
name string
|
||
|
interval TimeInterval
|
||
|
}{
|
||
|
{"Week", BalanceHistory7Days},
|
||
|
{"Month", BalanceHistory1Month},
|
||
|
{"HalfYear", BalanceHistory6Months},
|
||
|
{"Year", BalanceHistory1Year},
|
||
|
{"AllTime", BalanceHistoryAllTime},
|
||
|
}
|
||
|
for _, testInput := range testData {
|
||
|
t.Run(testInput.name, func(t *testing.T) {
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 0, len(balanceData))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryGetWithoutOverlappingFetch(t *testing.T) {
|
||
|
testData := []struct {
|
||
|
name string
|
||
|
interval TimeInterval
|
||
|
}{
|
||
|
{"Week", BalanceHistory7Days},
|
||
|
{"Month", BalanceHistory1Month},
|
||
|
{"HalfYear", BalanceHistory6Months},
|
||
|
{"Year", BalanceHistory1Year},
|
||
|
{"AllTime", BalanceHistoryAllTime},
|
||
|
}
|
||
|
for _, testInput := range testData {
|
||
|
t.Run(testInput.name, func(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
getUntilTimestamp := currentTimestamp - int64((400 /*days*/ * 24 * time.Hour).Seconds())
|
||
|
|
||
|
fetchInterval := testInput.interval + 3
|
||
|
if fetchInterval > BalanceHistoryAllTime {
|
||
|
fetchInterval = BalanceHistory7Days + BalanceHistoryAllTime - testInput.interval
|
||
|
}
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, fetchInterval)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, getUntilTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 0, len(balanceData))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryGetWithOverlappingFetch(t *testing.T) {
|
||
|
testData := []struct {
|
||
|
name string
|
||
|
interval TimeInterval
|
||
|
lessDaysToGet int
|
||
|
}{
|
||
|
{"Week", BalanceHistory7Days, 6},
|
||
|
{"Month", BalanceHistory1Month, 1},
|
||
|
{"HalfYear", BalanceHistory6Months, 8},
|
||
|
{"Year", BalanceHistory1Year, 16},
|
||
|
{"AllTime", BalanceHistoryAllTime, 130},
|
||
|
}
|
||
|
for _, testInput := range testData {
|
||
|
t.Run(testInput.name, func(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
olderUntilTimestamp := currentTimestamp - int64((time.Duration(testInput.lessDaysToGet) * 24 * time.Hour).Seconds())
|
||
|
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
||
|
|
||
|
olderBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, olderUntilTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.Less(t, len(olderBalanceData), len(balanceData))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryFetchFirstTime(t *testing.T) {
|
||
|
testData := []struct {
|
||
|
name string
|
||
|
interval TimeInterval
|
||
|
}{
|
||
|
{"Week", BalanceHistory7Days},
|
||
|
{"Month", BalanceHistory1Month},
|
||
|
{"HalfYear", BalanceHistory6Months},
|
||
|
{"Year", BalanceHistory1Year},
|
||
|
{"AllTime", BalanceHistoryAllTime},
|
||
|
}
|
||
|
for _, testInput := range testData {
|
||
|
t.Run(testInput.name, func(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
||
|
|
||
|
reqBlkNos, headerInfos, balances := extractTestData(dataSource)
|
||
|
require.Equal(t, len(balanceData), len(balances))
|
||
|
|
||
|
// Ensure we don't request the same info twice
|
||
|
for block, count := range headerInfos {
|
||
|
require.Equal(t, 1, count, "block %d has one info request", block)
|
||
|
if balanceCount, contains := balances[block]; contains {
|
||
|
require.Equal(t, 1, balanceCount, "block %d has one balance request", block)
|
||
|
}
|
||
|
}
|
||
|
for block, count := range balances {
|
||
|
require.Equal(t, 1, count, "block %d has one request", block)
|
||
|
}
|
||
|
|
||
|
resIdx := 0
|
||
|
for i := 0; i < len(reqBlkNos); i++ {
|
||
|
n := reqBlkNos[i]
|
||
|
rB := dataSource.requestedBlocks[n]
|
||
|
|
||
|
if _, contains := balances[n]; contains {
|
||
|
require.Equal(t, rB.time, balanceData[resIdx].Timestamp)
|
||
|
if resIdx > 0 {
|
||
|
require.Greater(t, balanceData[resIdx].Timestamp, balanceData[resIdx-1].Timestamp, "result timestamps are in order")
|
||
|
}
|
||
|
resIdx++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
errorFromIdeal := getTimeError(dataSource, balanceData, testInput.interval)
|
||
|
require.Less(t, math.Abs(float64(errorFromIdeal)), strideDuration(testInput.interval).Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, strideDuration(testInput.interval).Seconds(), testInput.interval)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryFetchError(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
bkFn := dataSource.balanceAtFn
|
||
|
// Fail first request
|
||
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
return nil, errors.New("test error")
|
||
|
}
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
||
|
require.Error(t, err, "Expect \"test error\"")
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 0, len(balanceData))
|
||
|
|
||
|
_, headerInfos, balances := extractTestData(dataSource)
|
||
|
require.Equal(t, 0, len(balances))
|
||
|
require.Equal(t, 1, len(headerInfos))
|
||
|
|
||
|
dataSource.resetStats()
|
||
|
// Fail later
|
||
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
if len(dataSource.requestedBlocks) == 15 {
|
||
|
return nil, errors.New("test error")
|
||
|
}
|
||
|
return dataSource.BalanceAtMock(ctx, account, blockNumber)
|
||
|
}
|
||
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
||
|
require.Error(t, err, "Expect \"test error\"")
|
||
|
|
||
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 14, len(balanceData))
|
||
|
|
||
|
reqBlkNos, headerInfos, balances := extractTestData(dataSource)
|
||
|
// The request for block info is made before the balance request
|
||
|
require.Equal(t, 1, dataSource.requestedBlocks[reqBlkNos[0]].headerInfoRequests)
|
||
|
require.Equal(t, 0, dataSource.requestedBlocks[reqBlkNos[0]].balanceRequests)
|
||
|
require.Equal(t, 14, len(balances))
|
||
|
require.Equal(t, len(balances), len(headerInfos)-1)
|
||
|
|
||
|
dataSource.resetStats()
|
||
|
dataSource.balanceAtFn = bkFn
|
||
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(BalanceHistory1Year))
|
||
|
|
||
|
_, headerInfos, balances = extractTestData(dataSource)
|
||
|
// Account for cache hits
|
||
|
require.Equal(t, len(balanceData)-14, len(balances))
|
||
|
require.Equal(t, len(balances), len(headerInfos))
|
||
|
|
||
|
for i := 1; i < len(balanceData); i++ {
|
||
|
require.Greater(t, balanceData[i].Timestamp, balanceData[i-1].Timestamp, "result timestamps are in order")
|
||
|
}
|
||
|
|
||
|
errorFromIdeal := getTimeError(dataSource, balanceData, BalanceHistory1Year)
|
||
|
require.Less(t, math.Abs(float64(errorFromIdeal)), strideDuration(BalanceHistory1Year).Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, strideDuration(BalanceHistory1Year).Seconds(), BalanceHistory1Year)
|
||
|
}
|
||
|
|
||
|
func TestBalanceHistoryValidateBalanceValuesAndCacheHit(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 20 /*years*/)
|
||
|
currentTimestamp := dataSource.TimeNow()
|
||
|
requestedBalance := make(map[int64]*big.Int)
|
||
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
balance, err := dataSource.BalanceAtMock(ctx, account, blockNumber)
|
||
|
requestedBalance[blockNumber.Int64()] = new(big.Int).Set(balance)
|
||
|
return balance, err
|
||
|
}
|
||
|
|
||
|
testData := []struct {
|
||
|
name string
|
||
|
interval TimeInterval
|
||
|
}{
|
||
|
{"Week", BalanceHistory7Days},
|
||
|
{"Month", BalanceHistory1Month},
|
||
|
{"HalfYear", BalanceHistory6Months},
|
||
|
{"Year", BalanceHistory1Year},
|
||
|
{"AllTime", BalanceHistoryAllTime},
|
||
|
}
|
||
|
for _, testInput := range testData {
|
||
|
t.Run(testInput.name, func(t *testing.T) {
|
||
|
dataSource.resetStats()
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
||
|
|
||
|
reqBlkNos, headerInfos, _ := extractTestData(dataSource)
|
||
|
// Only first run is not affected by cache
|
||
|
if testInput.interval == BalanceHistory7Days {
|
||
|
require.Equal(t, len(balanceData), len(requestedBalance))
|
||
|
require.Equal(t, len(balanceData), len(headerInfos))
|
||
|
} else {
|
||
|
require.Greater(t, len(balanceData), len(requestedBalance))
|
||
|
require.Greater(t, len(balanceData), len(headerInfos))
|
||
|
}
|
||
|
|
||
|
resIdx := 0
|
||
|
// Check that balance values are the one requested
|
||
|
for i := 0; i < len(reqBlkNos); i++ {
|
||
|
n := reqBlkNos[i]
|
||
|
|
||
|
if value, contains := requestedBalance[n]; contains {
|
||
|
require.Equal(t, value.Cmp(balanceData[resIdx].Value.ToInt()), 0)
|
||
|
resIdx++
|
||
|
}
|
||
|
blockHeaderRequestCount := dataSource.requestedBlocks[n].headerInfoRequests
|
||
|
require.Less(t, blockHeaderRequestCount, 2)
|
||
|
blockBalanceRequestCount := dataSource.requestedBlocks[n].balanceRequests
|
||
|
require.Less(t, blockBalanceRequestCount, 2)
|
||
|
}
|
||
|
|
||
|
// Check that balance values are in order
|
||
|
for i := 1; i < len(balanceData); i++ {
|
||
|
require.Greater(t, balanceData[i].Value.ToInt().Cmp(balanceData[i-1].Value.ToInt()), 0, "expected balanceData[%d] > balanceData[%d] for interval %d", i, i-1, testInput.interval)
|
||
|
}
|
||
|
requestedBalance = make(map[int64]*big.Int)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGetBalanceHistoryUpdateLater(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
currentTime := getTestTime(t)
|
||
|
initialTime := currentTime
|
||
|
moreThanADay := 24*time.Hour + 15*time.Minute
|
||
|
moreThanAMonth := 401 * moreThanADay
|
||
|
initialTime = initialTime.Add(-moreThanADay - moreThanAMonth)
|
||
|
dataSource := newTestSourceWithCurrentTime(t, 20 /*years*/, initialTime.Unix())
|
||
|
|
||
|
err := bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
prevBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(prevBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
||
|
|
||
|
// Advance little bit more than a day
|
||
|
later := initialTime
|
||
|
later = later.Add(moreThanADay)
|
||
|
dataSource.setCurrentTime(later.Unix())
|
||
|
dataSource.resetStats()
|
||
|
|
||
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
updatedBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(updatedBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
||
|
|
||
|
reqBlkNos, blockInfos, balances := extractTestData(dataSource)
|
||
|
require.Equal(t, 2, len(reqBlkNos))
|
||
|
require.Equal(t, len(reqBlkNos), len(blockInfos))
|
||
|
require.Equal(t, len(blockInfos), len(balances))
|
||
|
|
||
|
for block, count := range balances {
|
||
|
require.Equal(t, 1, count, "block %d has one request", block)
|
||
|
}
|
||
|
|
||
|
resIdx := len(updatedBalanceData) - 2
|
||
|
for i := 0; i < len(reqBlkNos); i++ {
|
||
|
rB := dataSource.requestedBlocks[reqBlkNos[i]]
|
||
|
|
||
|
// Ensure block approximation error doesn't exceed 10 blocks
|
||
|
require.Equal(t, 0.0, math.Abs(float64(int64(rB.time)-int64(updatedBalanceData[resIdx].Timestamp))))
|
||
|
if resIdx > 0 {
|
||
|
// Ensure result timestamps are in order
|
||
|
require.Greater(t, updatedBalanceData[resIdx].Timestamp, updatedBalanceData[resIdx-1].Timestamp)
|
||
|
}
|
||
|
resIdx++
|
||
|
}
|
||
|
|
||
|
errorFromIdeal := getTimeError(dataSource, updatedBalanceData, BalanceHistory1Month)
|
||
|
require.Less(t, math.Abs(float64(errorFromIdeal)), strideDuration(BalanceHistory1Month).Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, strideDuration(BalanceHistory1Month).Seconds(), BalanceHistory1Month)
|
||
|
|
||
|
// Advance little bit more than a month
|
||
|
dataSource.setCurrentTime(currentTime.Unix())
|
||
|
dataSource.resetStats()
|
||
|
|
||
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
newBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(newBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
||
|
|
||
|
_, headerInfos, balances := extractTestData(dataSource)
|
||
|
require.Greater(t, len(balances), len(newBalanceData), "Expected more balance requests due to missing time catch up")
|
||
|
|
||
|
// Ensure we don't request the same info twice
|
||
|
for block, count := range headerInfos {
|
||
|
require.Equal(t, 1, count, "block %d has one info request", block)
|
||
|
if balanceCount, contains := balances[block]; contains {
|
||
|
require.Equal(t, 1, balanceCount, "block %d has one balance request", block)
|
||
|
}
|
||
|
}
|
||
|
for block, count := range balances {
|
||
|
require.Equal(t, 1, count, "block %d has one request", block)
|
||
|
}
|
||
|
|
||
|
for i := 1; i < len(newBalanceData); i++ {
|
||
|
require.Greater(t, newBalanceData[i].Timestamp, newBalanceData[i-1].Timestamp, "result timestamps are in order")
|
||
|
}
|
||
|
|
||
|
errorFromIdeal = getTimeError(dataSource, newBalanceData, BalanceHistory1Month)
|
||
|
require.Less(t, math.Abs(float64(errorFromIdeal)), strideDuration(BalanceHistory1Month).Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, strideDuration(BalanceHistory1Month).Seconds(), BalanceHistory1Month)
|
||
|
}
|
||
|
|
||
|
func TestGetBalanceHistoryFetchMultipleAccounts(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
sevenDataSource := newTestSource(t, 5 /*years*/)
|
||
|
|
||
|
err := bh.update(context.Background(), sevenDataSource, common.Address{7}, BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
sevenBalanceData, err := bh.get(context.Background(), sevenDataSource.ChainID(), sevenDataSource.Currency(), common.Address{7}, sevenDataSource.TimeNow(), BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(sevenBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
||
|
|
||
|
_, sevenBlockInfos, _ := extractTestData(sevenDataSource)
|
||
|
require.Greater(t, len(sevenBlockInfos), 0)
|
||
|
|
||
|
nineDataSource := newTestSource(t, 5 /*years*/)
|
||
|
err = bh.update(context.Background(), nineDataSource, common.Address{9}, BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
nineBalanceData, err := bh.get(context.Background(), nineDataSource.ChainID(), nineDataSource.Currency(), common.Address{7}, nineDataSource.TimeNow(), BalanceHistory1Month)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(nineBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
||
|
|
||
|
_, nineBlockInfos, nineBalances := extractTestData(nineDataSource)
|
||
|
require.Equal(t, 0, len(nineBlockInfos))
|
||
|
require.Equal(t, len(nineBalanceData), len(nineBalances))
|
||
|
}
|
||
|
|
||
|
func TestGetBalanceHistoryUpdateCancellation(t *testing.T) {
|
||
|
bh, cleanDB := setupBalanceTest(t)
|
||
|
defer cleanDB()
|
||
|
|
||
|
dataSource := newTestSource(t, 5 /*years*/)
|
||
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||
|
bkFn := dataSource.balanceAtFn
|
||
|
// Fail after 15 requests
|
||
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
||
|
if len(dataSource.requestedBlocks) == 15 {
|
||
|
cancelFn()
|
||
|
}
|
||
|
return dataSource.BalanceAtMock(ctx, account, blockNumber)
|
||
|
}
|
||
|
err := bh.update(ctx, dataSource, common.Address{7}, BalanceHistory1Year)
|
||
|
require.Error(t, ctx.Err(), "Service canceled")
|
||
|
require.Error(t, err, "context cancelled")
|
||
|
|
||
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 15, len(balanceData))
|
||
|
|
||
|
_, blockInfos, balances := extractTestData(dataSource)
|
||
|
// The request for block info is made before the balance fails
|
||
|
require.Equal(t, 15, len(balances))
|
||
|
require.Equal(t, 15, len(blockInfos))
|
||
|
|
||
|
dataSource.balanceAtFn = bkFn
|
||
|
ctx, cancelFn = context.WithCancel(context.Background())
|
||
|
|
||
|
err = bh.update(ctx, dataSource, common.Address{7}, BalanceHistory1Year)
|
||
|
require.NoError(t, ctx.Err())
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Year)
|
||
|
require.NoError(t, err)
|
||
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(BalanceHistory1Year))
|
||
|
cancelFn()
|
||
|
}
|
||
|
|
||
|
func TestBlockStrideHaveCommonDivisor(t *testing.T) {
|
||
|
values := make([]blocksStride, 0, len(timeIntervalToStride))
|
||
|
for _, blockCount := range timeIntervalToStride {
|
||
|
values = append(values, blockCount)
|
||
|
}
|
||
|
sort.Slice(values, func(i, j int) bool {
|
||
|
return values[i] < values[j]
|
||
|
})
|
||
|
for i := 1; i < len(values); i++ {
|
||
|
require.Equal(t, blocksStride(0), values[i]%values[i-1], " %d value from index %d is divisible with previous %d", values[i], i, values[i-1])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBlockStrideMatchesBitsetFilter(t *testing.T) {
|
||
|
filterToStrideEquivalence := map[bitsetFilter]blocksStride{
|
||
|
filterAllTime: fourMonthsStride,
|
||
|
filterWeekly: weekStride,
|
||
|
filterTwiceADay: twiceADayStride,
|
||
|
}
|
||
|
|
||
|
for interval, bitsetFiler := range timeIntervalToBitsetFilter {
|
||
|
stride, found := timeIntervalToStride[interval]
|
||
|
require.True(t, found)
|
||
|
require.Equal(t, stride, filterToStrideEquivalence[bitsetFiler])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestTimeIntervalToBitsetFilterAreConsecutiveFlags(t *testing.T) {
|
||
|
values := make([]int, 0, len(timeIntervalToBitsetFilter))
|
||
|
for i := BalanceHistoryAllTime; i >= BalanceHistory7Days; i-- {
|
||
|
values = append(values, int(timeIntervalToBitsetFilter[i]))
|
||
|
}
|
||
|
|
||
|
for i := 0; i < len(values); i++ {
|
||
|
// count number of bits set
|
||
|
count := 0
|
||
|
for j := 0; j <= 30; j++ {
|
||
|
if values[i]&(1<<j) != 0 {
|
||
|
count++
|
||
|
}
|
||
|
}
|
||
|
require.Equal(t, 1, count, "%b value from index %d has only one bit set", values[i], i)
|
||
|
|
||
|
if i > 0 {
|
||
|
require.GreaterOrEqual(t, values[i], values[i-1], "%b value from index %d is higher then previous %d", values[i], i, values[i-1])
|
||
|
}
|
||
|
}
|
||
|
}
|