diff --git a/db/migration/migrate.go b/db/migration/migrate.go index b87dee4c..089aec94 100644 --- a/db/migration/migrate.go +++ b/db/migration/migrate.go @@ -18,7 +18,7 @@ func Up(db db.ExcecutorDb) error { $$ SELECT * FROM transaction WHERE (slot < "end_slot" AND slot >= "start_slot") AND - involved_accounts @> addresses + involved_accounts @> addresses ORDER BY slot+0 DESC $$ LANGUAGE sql STABLE; `) return err diff --git a/db/schema/00-solana.sql b/db/schema/00-solana.sql index e7fb5906..a51d3723 100644 --- a/db/schema/00-solana.sql +++ b/db/schema/00-solana.sql @@ -62,7 +62,7 @@ CREATE FUNCTION transactions_by_address( $$ SELECT * FROM transaction WHERE (slot <= "end_slot" AND slot >= "start_slot") AND - involved_accounts @> addresses + involved_accounts @> addresses ORDER BY slot+0 DESC $$ LANGUAGE sql STABLE; /** diff --git a/modules/bank/handle_periodic_operations.go b/modules/bank/handle_periodic_operations.go index 2531e2e7..6f965b0c 100644 --- a/modules/bank/handle_periodic_operations.go +++ b/modules/bank/handle_periodic_operations.go @@ -3,6 +3,7 @@ package bank import ( "fmt" + "github.com/forbole/soljuno/modules/utils" "github.com/go-co-op/gocron" "github.com/rs/zerolog/log" ) @@ -12,24 +13,21 @@ import ( func (m *Module) RegisterPeriodicOperations(scheduler *gocron.Scheduler) error { log.Debug().Str("module", m.Name()).Msg("setting up periodic tasks") if _, err := scheduler.Every(10).Second().Do(func() { - m.HandlePeriodicOperations() + utils.WatchMethod(m, m.HandlePeriodicOperations) }); err != nil { return fmt.Errorf("error while setting up bank periodic operation: %s", err) } return nil } -func (m *Module) HandlePeriodicOperations() { +func (m *Module) HandlePeriodicOperations() error { m.mtx.Lock() defer m.mtx.Unlock() balances := m.BalanceEntries tokenBalances := m.TokenBalanceEntries m.BalanceEntries = nil m.TokenBalanceEntries = nil - err := m.updateBalances(balances, tokenBalances) - if err != nil { - log.Error().Str("module", m.Name()).Err(err).Send() - } + return m.updateBalances(balances, tokenBalances) } func (m *Module) updateBalances(balances []AccountBalanceEntry, tokenBalances []TokenAccountBalanceEntry) error { diff --git a/modules/vote/commen_test.go b/modules/vote/commen_test.go new file mode 100644 index 00000000..fc73a348 --- /dev/null +++ b/modules/vote/commen_test.go @@ -0,0 +1,108 @@ +package vote_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/forbole/soljuno/db" + dbtypes "github.com/forbole/soljuno/db/types" + "github.com/forbole/soljuno/modules/vote" + clienttypes "github.com/forbole/soljuno/solana/client/types" +) + +var _ db.VoteDb = &MockDb{} + +type MockDb struct { + isLatest bool + err error +} + +func NewDefaultMockDb() *MockDb { + return &MockDb{isLatest: true} +} + +func (db *MockDb) SaveValidator(account dbtypes.VoteAccountRow) error { return db.err } +func (db *MockDb) SaveValidatorStatuses(statuses []dbtypes.ValidatorStatusRow) error { return db.err } +func (db *MockDb) GetEpochProducedBlocks(epoch uint64) ([]uint64, error) { return []uint64{0}, db.err } +func (db *MockDb) SaveValidatorSkipRates(skipRates []dbtypes.ValidatorSkipRateRow) error { + return db.err +} +func (db *MockDb) SaveHistoryValidatorSkipRates(skipRates []dbtypes.ValidatorSkipRateRow) error { + return db.err +} + +func (db *MockDb) CheckValidatorLatest(address string, currentSlot uint64) bool { + return db.isLatest +} + +func (m MockDb) GetCached() MockDb { + return m +} + +func (m *MockDb) WithError(err error) { + m.err = err +} + +func (m *MockDb) WithLatest(isLatest bool) { + m.isLatest = isLatest +} + +// ---------------------------------------------------------------- + +var _ vote.ClientProxy = &MockClient{} + +type MockClient struct { + account clienttypes.AccountInfo + err error +} + +func NewDefaultMockClient() *MockClient { + return &MockClient{} +} + +func (m MockClient) GetCached() MockClient { + return m +} + +func (m *MockClient) WithError(err error) { + m.err = err +} + +func (m *MockClient) WithAccount(account clienttypes.AccountInfo) { + m.account = account +} + +func (m *MockClient) GetAccountInfo(address string) (clienttypes.AccountInfo, error) { + return m.account, m.err +} + +func (m *MockClient) GetVoteAccountsWithSlot() (uint64, clienttypes.VoteAccounts, error) { + return 0, clienttypes.VoteAccounts{ + Current: []clienttypes.VoteAccount{{VotePubkey: "current"}}, + Delinquent: []clienttypes.VoteAccount{{VotePubkey: "delinquent"}}, + }, m.err +} + +func (m *MockClient) GetLeaderSchedule(uint64) (clienttypes.LeaderSchedule, error) { + return clienttypes.LeaderSchedule{"address": []int{0, 1}}, m.err +} + +// ---------------------------------------------------------------- + +type ModuleTestSuite struct { + suite.Suite + module *vote.Module + db *MockDb + client *MockClient +} + +func TestModuleTestSuite(t *testing.T) { + suite.Run(t, new(ModuleTestSuite)) +} + +func (suite *ModuleTestSuite) SetupTest() { + suite.module = vote.NewModule(NewDefaultMockDb(), NewDefaultMockClient()) + suite.db = NewDefaultMockDb() + suite.client = NewDefaultMockClient() +} diff --git a/modules/vote/epoch_service.go b/modules/vote/epoch_service.go index 9b5e128f..d5911e01 100644 --- a/modules/vote/epoch_service.go +++ b/modules/vote/epoch_service.go @@ -1,66 +1,5 @@ package vote -import ( - "fmt" - - dbtypes "github.com/forbole/soljuno/db/types" - solanatypes "github.com/forbole/soljuno/solana/types" -) - func (m *Module) ExecEpoch(epoch uint64) error { - return m.updateValidatorSkipRates(epoch - 1) -} - -// updateValidatorSkipRates properly stores the skip rates of all validators inside the database -func (m *Module) updateValidatorSkipRates(epoch uint64) error { - slots, err := m.db.GetEpochProducedBlocks(epoch) - if err != nil { - return err - } - if len(slots) == 0 { - return fmt.Errorf("%d epoch blocks does not exist", epoch) - } - - endSlot := slots[len(slots)-1] - schedules, err := m.client.GetLeaderSchedule(epoch * solanatypes.SlotsInEpoch) - if err != nil { - return err - } - - produced := make(map[int]bool) - for _, slot := range slots { - produced[int(slot%solanatypes.SlotsInEpoch)] = true - } - - skipRateRows := make([]dbtypes.ValidatorSkipRateRow, len(schedules)) - count := 0 - end := int(endSlot % solanatypes.SlotsInEpoch) - for validator, schedule := range schedules { - total, skip := getSkipRateReference(end, produced, schedule) - skipRate := float64(skip) / float64(total) - skipRateRows[count] = dbtypes.NewValidatorSkipRateRow(validator, epoch, skipRate, total, skip) - count++ - } - - err = m.db.SaveValidatorSkipRates(skipRateRows) - if err != nil { - return err - } - return m.db.SaveHistoryValidatorSkipRates(skipRateRows) -} - -// getSkipRateReference returns the total and skip amount in a epoch of the validator from the given produced map and the validator schedule -func getSkipRateReference(end int, produced map[int]bool, schedule []int) (int, int) { - var skip int = 0 - var total int = 0 - for _, slotInEpoch := range schedule { - total++ - if slotInEpoch > end { - break - } - if ok := produced[slotInEpoch]; !ok { - skip++ - } - } - return total, skip + return UpdateValidatorSkipRates(epoch-1, m.db, m.client) } diff --git a/modules/vote/handle_instruction.go b/modules/vote/handle_instruction.go index 17ff8168..dbaca971 100644 --- a/modules/vote/handle_instruction.go +++ b/modules/vote/handle_instruction.go @@ -5,32 +5,31 @@ import ( "github.com/forbole/soljuno/db" dbtypes "github.com/forbole/soljuno/db/types" - "github.com/forbole/soljuno/solana/client" "github.com/forbole/soljuno/solana/program/vote" "github.com/forbole/soljuno/types" ) // HandleInstruction allows to handle different instructions types for the vote module -func HandleInstruction(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func HandleInstruction(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { switch instruction.Parsed.Type { case "initialize": - return handleInitialize(instruction, tx, db) + return handleInitialize(instruction, db) case "authorize": - return handleAuthorize(instruction, tx, db, client) + return handleAuthorize(instruction, db, client) case "withdraw": - return handleWithdraw(instruction, tx, db, client) + return handleWithdraw(instruction, db, client) case "updateValidatorIdentity": - return handleUpdateValidatorIdentity(instruction, tx, db, client) + return handleUpdateValidatorIdentity(instruction, db, client) case "updateCommission": - return handleUpdateCommission(instruction, tx, db, client) + return handleUpdateCommission(instruction, db, client) case "authorizeChecked": - return handleAuthorizeChecked(instruction, tx, db, client) + return handleAuthorizeChecked(instruction, db, client) } return nil } // handleInitialize handles a instruction of Initialize -func handleInitialize(instruction types.Instruction, tx types.Tx, db db.VoteDb) error { +func handleInitialize(instruction types.Instruction, db db.VoteDb) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedInitializeAccount) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "initialize", instruction.Parsed.Type) @@ -38,57 +37,57 @@ func handleInitialize(instruction types.Instruction, tx types.Tx, db db.VoteDb) } return db.SaveValidator( dbtypes.NewVoteAccountRow( - parsed.VoteAccount, tx.Slot, parsed.Node, parsed.AuthorizedVoter, parsed.AuthorizedWithdrawer, parsed.Commission, + parsed.VoteAccount, instruction.Slot, parsed.Node, parsed.AuthorizedVoter, parsed.AuthorizedWithdrawer, parsed.Commission, ), ) } // handleAuthorize handles a instruction of Authorize -func handleAuthorize(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func handleAuthorize(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedAuthorize) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "authorize", instruction.Parsed.Type) } - return updateVoteAccount(parsed.VoteAccount, tx.Slot, db, client) + return UpdateVoteAccount(parsed.VoteAccount, instruction.Slot, db, client) } // handleWithdraw handles a instruction of Withdraw -func handleWithdraw(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func handleWithdraw(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedWithdraw) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "withdraw", instruction.Parsed.Type) } - return updateVoteAccount(parsed.VoteAccount, tx.Slot, db, client) + return UpdateVoteAccount(parsed.VoteAccount, instruction.Slot, db, client) } // handleUpdateValidatorIdentity handles a instruction of UpdateValidatorIdentity -func handleUpdateValidatorIdentity(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func handleUpdateValidatorIdentity(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedUpdateValidatorIdentity) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "updateValidatorIdentity", instruction.Parsed.Type) } - return updateVoteAccount(parsed.VoteAccount, tx.Slot, db, client) + return UpdateVoteAccount(parsed.VoteAccount, instruction.Slot, db, client) } // handleUpdateCommission handles a instruction of UpdateCommission -func handleUpdateCommission(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func handleUpdateCommission(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedUpdateCommission) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "updateCommission", instruction.Parsed.Type) } - return updateVoteAccount(parsed.VoteAccount, tx.Slot, db, client) + return UpdateVoteAccount(parsed.VoteAccount, instruction.Slot, db, client) } // handleAuthorizeChecked handles a instruction of AuthorizeChecked -func handleAuthorizeChecked(instruction types.Instruction, tx types.Tx, db db.VoteDb, client client.ClientProxy) error { +func handleAuthorizeChecked(instruction types.Instruction, db db.VoteDb, client ClientProxy) error { parsed, ok := instruction.Parsed.Value.(vote.ParsedAuthorizeChecked) if !ok { return fmt.Errorf("instruction does not match %s type: %s", "authorizeChecked", instruction.Parsed.Type) } - return updateVoteAccount(parsed.VoteAccount, tx.Slot, db, client) + return UpdateVoteAccount(parsed.VoteAccount, instruction.Slot, db, client) } diff --git a/modules/vote/handle_instruction_test.go b/modules/vote/handle_instruction_test.go new file mode 100644 index 00000000..66de6ad2 --- /dev/null +++ b/modules/vote/handle_instruction_test.go @@ -0,0 +1,120 @@ +package vote_test + +import ( + "github.com/forbole/soljuno/modules/vote" + voteProgram "github.com/forbole/soljuno/solana/program/vote" + solanatypes "github.com/forbole/soljuno/solana/types" + "github.com/forbole/soljuno/types" +) + +func (suite *ModuleTestSuite) Test_HandleInstruction() { + testCases := []struct { + name string + data types.Instruction + }{ + { + name: "initialize instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "initialize", + voteProgram.ParsedInitializeAccount{}, + ), + ), + }, + { + name: "authorize instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "authorize", + voteProgram.ParsedAuthorize{}, + ), + ), + }, + { + name: "delegate instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "withdraw", + voteProgram.ParsedWithdraw{}, + ), + ), + }, + { + name: "split instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "updateValidatorIdentity", + voteProgram.ParsedUpdateValidatorIdentity{}, + ), + ), + }, + { + name: "withdraw instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "updateCommission", + voteProgram.ParsedUpdateCommission{}, + ), + ), + }, + { + name: "deactivate instruction works properly", + data: types.NewInstruction( + "sig", + 1, + 0, + 0, + "vote", + []string{}, + "", + solanatypes.NewParsedInstruction( + "authorizeChecked", + voteProgram.ParsedAuthorizeChecked{}, + ), + ), + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + err := vote.HandleInstruction(tc.data, suite.db, suite.client) + suite.Require().NoError(err) + }) + } +} diff --git a/modules/vote/handle_periodic_operations.go b/modules/vote/handle_periodic_operations.go index d400538f..25c4a75a 100644 --- a/modules/vote/handle_periodic_operations.go +++ b/modules/vote/handle_periodic_operations.go @@ -6,8 +6,6 @@ import ( "github.com/forbole/soljuno/modules/utils" "github.com/go-co-op/gocron" "github.com/rs/zerolog/log" - - dbtypes "github.com/forbole/soljuno/db/types" ) // RegisterPeriodicOperations implements modules.Module @@ -15,7 +13,7 @@ func (m *Module) RegisterPeriodicOperations(scheduler *gocron.Scheduler) error { log.Debug().Str("module", "vote").Msg("setting up periodic tasks") if _, err := scheduler.Every(1).Minute().Do(func() { - utils.WatchMethod(m, m.updateValidatorsStatus) + utils.WatchMethod(m, m.handlePeriodicOperations) }); err != nil { return fmt.Errorf("error while setting up vote periodic operation: %s", err) } @@ -23,37 +21,6 @@ func (m *Module) RegisterPeriodicOperations(scheduler *gocron.Scheduler) error { return nil } -// updateValidatorsStatus insert current validators status -func (m *Module) updateValidatorsStatus() error { - slot, voteAccounts, err := m.client.GetVoteAccountsWithSlot() - if err != nil { - return nil - } - - statuses := make([]dbtypes.ValidatorStatusRow, len(voteAccounts.Current)+len(voteAccounts.Delinquent)) - count := 0 - for _, account := range voteAccounts.Current { - statuses[count] = dbtypes.NewValidatorStatusRow( - account.VotePubkey, - slot, - account.ActivatedStake, - account.LastVote, - account.RootSlot, - true, - ) - count++ - } - - for _, account := range voteAccounts.Delinquent { - statuses[count] = dbtypes.NewValidatorStatusRow( - account.VotePubkey, - slot, - account.ActivatedStake, - account.LastVote, - account.RootSlot, - false, - ) - count++ - } - return m.db.SaveValidatorStatuses(statuses) +func (m *Module) handlePeriodicOperations() error { + return UpdateValidatorsStatus(m.db, m.client) } diff --git a/modules/vote/module.go b/modules/vote/module.go index 2d5c7cb5..3847618a 100644 --- a/modules/vote/module.go +++ b/modules/vote/module.go @@ -3,12 +3,18 @@ package vote import ( "github.com/forbole/soljuno/db" "github.com/forbole/soljuno/modules" - "github.com/forbole/soljuno/solana/client" + clienttypes "github.com/forbole/soljuno/solana/client/types" "github.com/forbole/soljuno/solana/program/vote" "github.com/forbole/soljuno/types" "github.com/rs/zerolog/log" ) +type ClientProxy interface { + GetAccountInfo(string) (clienttypes.AccountInfo, error) + GetVoteAccountsWithSlot() (uint64, clienttypes.VoteAccounts, error) + GetLeaderSchedule(uint64) (clienttypes.LeaderSchedule, error) +} + var ( _ modules.Module = &Module{} _ modules.InstructionModule = &Module{} @@ -16,11 +22,11 @@ var ( ) type Module struct { - db db.Database - client client.ClientProxy + db db.VoteDb + client ClientProxy } -func NewModule(db db.Database, client client.ClientProxy) *Module { +func NewModule(db db.VoteDb, client ClientProxy) *Module { return &Module{ db: db, client: client, @@ -41,7 +47,7 @@ func (m *Module) HandleInstruction(instruction types.Instruction, tx types.Tx) e return nil } - err := HandleInstruction(instruction, tx, m.db, m.client) + err := HandleInstruction(instruction, m.db, m.client) if err != nil { return err } diff --git a/modules/vote/module_test.go b/modules/vote/module_test.go new file mode 100644 index 00000000..d9f0a034 --- /dev/null +++ b/modules/vote/module_test.go @@ -0,0 +1,90 @@ +package vote_test + +import ( + "fmt" + + voteProgram "github.com/forbole/soljuno/solana/program/vote" + solanatypes "github.com/forbole/soljuno/solana/types" + "github.com/forbole/soljuno/types" +) + +func (suite *ModuleTestSuite) TestModule_Name() { + suite.Require().Equal("vote", suite.module.Name()) +} + +func (suite *ModuleTestSuite) TestModule_HandleInstruction() { + testCases := []struct { + name string + tx types.Tx + instruction types.Instruction + shouldErr bool + }{ + { + name: "failed tx skip properly", + tx: types.NewTx("sig", 0, fmt.Errorf("failed"), 0, nil, nil, nil, nil, nil), + }, + { + name: "non stake instruction skips properly", + instruction: types.NewInstruction( + "sig", + 1, + 0, + 0, + "unknown", + nil, + "", + solanatypes.NewParsedInstruction( + "initialize", + nil, + ), + ), + }, + { + name: "fail to handle instruction return error", + tx: types.NewTx("sig", 0, nil, 0, nil, nil, nil, nil, nil), + instruction: types.NewInstruction( + "sig", + 1, + 0, + 0, + voteProgram.ProgramID, + []string{}, + "", + solanatypes.NewParsedInstruction( + "initialize", + nil, + ), + ), + shouldErr: true, + }, + { + name: "instruction works properly", + tx: types.NewTx("sig", 0, nil, 0, nil, nil, nil, nil, nil), + instruction: types.NewInstruction( + "sig", + 1, + 0, + 0, + voteProgram.ProgramID, + []string{}, + "", + solanatypes.NewParsedInstruction( + "initialize", + voteProgram.ParsedInitializeAccount{}, + ), + ), + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + err := suite.module.HandleInstruction(tc.instruction, tc.tx) + if tc.shouldErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/modules/vote/utils.go b/modules/vote/utils.go index 44e82752..bc571199 100644 --- a/modules/vote/utils.go +++ b/modules/vote/utils.go @@ -2,15 +2,16 @@ package vote import ( "encoding/base64" + "fmt" "github.com/forbole/soljuno/db" dbtypes "github.com/forbole/soljuno/db/types" "github.com/forbole/soljuno/solana/account/parser" - "github.com/forbole/soljuno/solana/client" + solanatypes "github.com/forbole/soljuno/solana/types" ) -// updateVoteAccount properly stores the statement of vote account inside the database -func updateVoteAccount(address string, currentSlot uint64, db db.VoteDb, client client.ClientProxy) error { +// UpdateVoteAccount properly stores the statement of vote account inside the database +func UpdateVoteAccount(address string, currentSlot uint64, db db.VoteDb, client ClientProxy) error { if db.CheckValidatorLatest(address, currentSlot) { return nil } @@ -40,3 +41,92 @@ func updateVoteAccount(address string, currentSlot uint64, db db.VoteDb, client ), ) } + +// UpdateValidatorsStatus insert current validators status +func UpdateValidatorsStatus(db db.VoteDb, client ClientProxy) error { + slot, voteAccounts, err := client.GetVoteAccountsWithSlot() + if err != nil { + return err + } + + statuses := make([]dbtypes.ValidatorStatusRow, len(voteAccounts.Current)+len(voteAccounts.Delinquent)) + count := 0 + for _, account := range voteAccounts.Current { + statuses[count] = dbtypes.NewValidatorStatusRow( + account.VotePubkey, + slot, + account.ActivatedStake, + account.LastVote, + account.RootSlot, + true, + ) + count++ + } + + for _, account := range voteAccounts.Delinquent { + statuses[count] = dbtypes.NewValidatorStatusRow( + account.VotePubkey, + slot, + account.ActivatedStake, + account.LastVote, + account.RootSlot, + false, + ) + count++ + } + return db.SaveValidatorStatuses(statuses) +} + +// UpdateValidatorSkipRates properly stores the skip rates of all validators inside the database +func UpdateValidatorSkipRates(lastEpoch uint64, db db.VoteDb, client ClientProxy) error { + slots, err := db.GetEpochProducedBlocks(lastEpoch) + if err != nil { + return err + } + if len(slots) == 0 { + return fmt.Errorf("%d epoch blocks does not exist", lastEpoch) + } + + endSlot := slots[len(slots)-1] + schedules, err := client.GetLeaderSchedule(lastEpoch * solanatypes.SlotsInEpoch) + if err != nil { + return err + } + + produced := make(map[int]bool) + for _, slot := range slots { + produced[int(slot%solanatypes.SlotsInEpoch)] = true + } + + skipRateRows := make([]dbtypes.ValidatorSkipRateRow, len(schedules)) + count := 0 + end := int(endSlot % solanatypes.SlotsInEpoch) + for validator, schedule := range schedules { + total, skip := GetSkipRateReference(end, produced, schedule) + skipRate := float64(skip) / float64(total) + skipRateRows[count] = dbtypes.NewValidatorSkipRateRow(validator, lastEpoch, skipRate, total, skip) + count++ + } + + err = db.SaveValidatorSkipRates(skipRateRows) + if err != nil { + return err + } + return db.SaveHistoryValidatorSkipRates(skipRateRows) +} + +// GetSkipRateReference returns the total and skip amount in a epoch of the validator from the given produced map and the validator schedule +func GetSkipRateReference(end int, produced map[int]bool, schedule []int) (int, int) { + var skip int = 0 + var total int = 0 + for _, slotInEpoch := range schedule { + total++ + if slotInEpoch > end { + break + } + if ok := produced[slotInEpoch]; !ok { + skip++ + } + } + return total, skip +} diff --git a/modules/vote/utils_test.go b/modules/vote/utils_test.go new file mode 100644 index 00000000..ea349a6e --- /dev/null +++ b/modules/vote/utils_test.go @@ -0,0 +1,117 @@ +package vote_test + +import ( + "fmt" + + "github.com/forbole/soljuno/modules/vote" + clienttypes "github.com/forbole/soljuno/solana/client/types" + voteProgram "github.com/forbole/soljuno/solana/program/vote" +) + +func (suite *ModuleTestSuite) TestModule_UpdateVoteAccount() { + testCases := []struct { + name string + isLatest bool + account clienttypes.AccountInfo + shouldErr bool + }{ + { + name: "skip updating returns no error", + isLatest: true, + }, + { + name: "receive empty account data and delete account properly", + isLatest: false, + }, + { + name: "fail to decode data returns error", + isLatest: false, + account: clienttypes.AccountInfo{ + Value: &clienttypes.AccountValue{ + Data: [2]string{"$invalid", "base64"}, + }, + }, + shouldErr: true, + }, + { + name: "receive non valid account and delete account properly", + isLatest: false, + account: clienttypes.AccountInfo{ + Value: &clienttypes.AccountValue{ + Data: [2]string{"dW5rbm93bg==", "base64"}, + Owner: "unknown", + }, + }, + }, + { + name: "receive valid account and update account properly", + isLatest: false, + account: clienttypes.AccountInfo{ + Value: &clienttypes.AccountValue{ + Data: [2]string{"AQAAAElFniOxic6SFmHGLHEOtebZhevbyiTVZIkb3SPlIcfsY+skleJmc/3ITI9/AuXqYw4K10Be6PIILZKSxuclUm4KHwAAAAAAAACE0dYHAAAAAB8AAACF0dYHAAAAAB4AAACG0dYHAAAAAB0AAACH0dYHAAAAABwAAACI0dYHAAAAABsAAACJ0dYHAAAAABoAAACK0dYHAAAAABkAAACL0dYHAAAAABgAAACN0dYHAAAAABcAAACO0dYHAAAAABYAAACP0dYHAAAAABUAAACQ0dYHAAAAABQAAACR0dYHAAAAABMAAACS0dYHAAAAABIAAACT0dYHAAAAABEAAACU0dYHAAAAABAAAACV0dYHAAAAAA8AAACW0dYHAAAAAA4AAACX0dYHAAAAAA0AAACY0dYHAAAAAAwAAACZ0dYHAAAAAAsAAACw0dYHAAAAAAoAAACx0dYHAAAAAAkAAACy0dYHAAAAAAgAAACz0dYHAAAAAAcAAAC00dYHAAAAAAYAAAC10dYHAAAAAAUAAAC20dYHAAAAAAQAAAC30dYHAAAAAAMAAAC40dYHAAAAAAIAAAC50dYHAAAAAAEAAAABg9HWBwAAAAABAAAAAAAAADABAAAAAAAASUWeI7GJzpIWYcYscQ615tmF69vKJNVkiRvdI+Uhx+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAEfAAAAAAAAABIBAAAAAAAApmEFAAAAAAAAAAAAAAAAABMBAAAAAAAAkwULAAAAAACmYQUAAAAAABQBAAAAAAAAzN8QAAAAAACTBQsAAAAAABUBAAAAAAAAgYAWAAAAAADM3xAAAAAAABYBAAAAAAAADBwcAAAAAACBgBYAAAAAABcBAAAAAAAA57whAAAAAAAMHBwAAAAAABgBAAAAAAAAnFcnAAAAAADnvCEAAAAAABkBAAAAAAAAosksAAAAAACcVycAAAAAABoBAAAAAAAAZyIyAAAAAACiySwAAAAAABsBAAAAAAAA4mo3AAAAAABnIjIAAAAAABwBAAAAAAAAl7g8AAAAAADiajcAAAAAAB0BAAAAAAAAO/tBAAAAAACXuDwAAAAAAB4BAAAAAAAAnGxHAAAAAAA7+0EAAAAAAB8BAAAAAAAAzplMAAAAAACcbEcAAAAAACABAAAAAAAAkS1SAAAAAADOmUwAAAAAACEBAAAAAAAA76lXAAAAAACRLVIAAAAAACIBAAAAAAAAvq5dAAAAAADvqVcAAAAAACMBAAAAAAAA86hjAAAAAAC+rl0AAAAAACQBAAAAAAAAJcZpAAAAAADzqGMAAAAAACUBAAAAAAAAxdpvAAAAAAAlxmkAAAAAACYBAAAAAAAA+Qx2AAAAAADF2m8AAAAAACcBAAAAAAAAiu17AAAAAAD5DHYAAAAAACgBAAAAAAAATkmBAAAAAACK7XsAAAAAACkBAAAAAAAAGCiGAAAAAABOSYEAAAAAACoBAAAAAAAAJleKAAAAAAAYKIYAAAAAACsBAAAAAAAAXHKPAAAAAAAmV4oAAAAAACwBAAAAAAAAiUKVAAAAAABcco8AAAAAAC0BAAAAAAAAU6yaAAAAAACJQpUAAAAAAC4BAAAAAAAAYG+gAAAAAABTrJoAAAAAAC8BAAAAAAAA8++lAAAAAABgb6AAAAAAADABAAAAAAAA0TOoAAAAAADz76UAAAAAALnR1gcAAAAAKQdpYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "base64"}, + Owner: voteProgram.ProgramID, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + db := suite.db.GetCached() + db.WithLatest(tc.isLatest) + + client := suite.client.GetCached() + client.WithAccount(tc.account) + + err := vote.UpdateVoteAccount("address", 1, &db, &client) + if tc.shouldErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *ModuleTestSuite) TestModule_UpdateValidatorsStatus() { + err := vote.UpdateValidatorsStatus(suite.db, suite.client) + suite.Require().NoError(err) + + // with error client returns error + errClient := suite.client.GetCached() + errClient.WithError(fmt.Errorf("error")) + err = vote.UpdateValidatorsStatus(suite.db, &errClient) + suite.Require().Error(err) + + // with error db returns error + errDb := suite.db.GetCached() + errDb.WithError(fmt.Errorf("error")) + err = vote.UpdateValidatorsStatus(&errDb, suite.client) + suite.Require().Error(err) +} + +func (suite *ModuleTestSuite) TestModule_UpdateValidatorSkipRates() { + err := vote.UpdateValidatorSkipRates(1, suite.db, suite.client) + suite.Require().NoError(err) + + // with error client returns error + errClient := suite.client.GetCached() + errClient.WithError(fmt.Errorf("error")) + err = vote.UpdateValidatorSkipRates(1, suite.db, &errClient) + suite.Require().Error(err) + + // with error db returns error + errDb := suite.db.GetCached() + errDb.WithError(fmt.Errorf("error")) + err = vote.UpdateValidatorSkipRates(1, &errDb, suite.client) + suite.Require().Error(err) +} + +func (suite *ModuleTestSuite) TestModule_GetSkipRateReference() { + m := make(map[int]bool) + m[0] = true + total, skip := vote.GetSkipRateReference(1, m, []int{0, 1}) + suite.Require().Equal(2, total) + suite.Require().Equal(1, skip) +}