2022-11-15 12:14:41 +00:00
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 {
2023-01-25 18:28:51 +00:00
require . Equal ( t , value . Cmp ( balanceData [ resIdx ] . Balance . ToInt ( ) ) , 0 )
2022-11-15 12:14:41 +00:00
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 ++ {
2023-01-25 18:28:51 +00:00
require . Greater ( t , balanceData [ i ] . Balance . ToInt ( ) . Cmp ( balanceData [ i - 1 ] . Balance . ToInt ( ) ) , 0 , "expected balanceData[%d] > balanceData[%d] for interval %d" , i , i - 1 , testInput . interval )
2022-11-15 12:14:41 +00:00
}
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 ] )
}
}
}