Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b49f4a
CAS-135: Add Leaderboard Endpoint
Jun 22, 2022
a68bb46
Merge branch 'CAS-135' of https://github.com/DapperCollectives/CAST i…
Jun 23, 2022
0a5144d
Merge branch 'main' into CAS-135
0xmovses Jun 27, 2022
c137947
CAS-135: Add leaderboard test file and remove unnecessary comments
Jun 28, 2022
25452e5
Merge branch 'CAS-135' of https://github.com/DapperCollectives/CAST i…
Jun 28, 2022
f39783a
Merge branch 'CAS-135' of https://github.com/DapperCollectives/CAST i…
Jun 28, 2022
e563cdb
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jun 28, 2022
93777c3
CAS-139: Add EarlyVote scoring
Jun 28, 2022
5ac3803
CAS-139: Add EarlyVote scoring
Jun 28, 2022
f2461ff
CAS-139: Updates based on PR Feedback
Jun 29, 2022
767e025
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jun 30, 2022
4f96e71
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jun 30, 2022
0d80115
Merge branch 'CAS-139' of https://github.com/DapperCollectives/CAST i…
Jun 30, 2022
cc5479d
CAS-140: Add Streak Bonus
Jul 5, 2022
ad1fcd8
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jul 5, 2022
f7879e6
CAS-140: Add helper functions for leaderboard test
Jul 5, 2022
c718a01
CAS-140: Refactor based on PR feedback
Jul 6, 2022
ada7c24
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jul 6, 2022
672d068
CAS-141: Add WinningVote Bonus
Jul 6, 2022
f2a627e
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jul 6, 2022
8006f9e
Merge branch 'CAS-140' of https://github.com/DapperCollectives/CAST i…
Jul 7, 2022
af1784e
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jul 7, 2022
57221c3
CAS-141: Update achievements query to use COALESCE
Jul 7, 2022
0c54e51
Merge branch 'main' of https://github.com/DapperCollectives/CAST into…
Jul 12, 2022
4cef23e
CAS-141: Update proposal status check to use Computed_status
Jul 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions backend/main/leaderboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package main
import (
"encoding/json"
"net/http"
"sort"
"testing"
"time"

"github.com/DapperCollectives/CAST/backend/main/test_utils"
"github.com/stretchr/testify/assert"
)

func TestGetCommunityLeaderboard(t *testing.T) {
func TestGetLeaderboard(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("user_achievements")
Expand Down Expand Up @@ -43,7 +45,7 @@ func TestGetCommunityLeaderboard(t *testing.T) {
assert.Equal(t, expectedScore, receivedUser2Score)
}

func TestGetCommunityLeaderboardWithEarlyVotes(t *testing.T) {
func TestGetLeaderboardWithEarlyVotes(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("user_achievements")
Expand Down Expand Up @@ -72,7 +74,7 @@ func TestGetCommunityLeaderboardWithEarlyVotes(t *testing.T) {
assert.Equal(t, expectedScore, receivedScore)
}

func TestGetCommunityLeaderboardWithSingleStreak(t *testing.T) {
func TestGetLeaderboardWithSingleStreak(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("user_achievements")
Expand All @@ -83,8 +85,8 @@ func TestGetCommunityLeaderboardWithSingleStreak(t *testing.T) {
streaks := []int{3, 4}
streakBonus := 1
expectedUsers := 2
expectedUser1Score := 3 + (1 * streakBonus)
expectedUser2Score := 4 + (1 * streakBonus)
expectedScoreA := streaks[0] + (1 * streakBonus)
expectedScoreB := streaks[1] + (1 * streakBonus)

otu.GenerateSingleStreakAchievements(communityId, streaks)

Expand All @@ -94,15 +96,20 @@ func TestGetCommunityLeaderboardWithSingleStreak(t *testing.T) {
var p test_utils.PaginatedResponseWithLeaderboardUser
json.Unmarshal(response.Body.Bytes(), &p)

receivedUser1Score := p.Data[0].Score
receivedUser2Score := p.Data[1].Score
// ensure scores ordered for assert
sort.Slice(p.Data, func(i, j int) bool {
return p.Data[i].Score < p.Data[j].Score
})

receivedScoreA := p.Data[0].Score
receivedScoreB := p.Data[1].Score

assert.Equal(t, expectedUsers, len(p.Data))
assert.Equal(t, expectedUser1Score, receivedUser1Score)
assert.Equal(t, expectedUser2Score, receivedUser2Score)
assert.Equal(t, expectedScoreA, receivedScoreA)
assert.Equal(t, expectedScoreB, receivedScoreB)
}

func TestGetCommunityLeaderboardWithMultiStreaks(t *testing.T) {
func TestGetLeaderboardWithMultiStreaks(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("user_achievements")
Expand All @@ -129,3 +136,44 @@ func TestGetCommunityLeaderboardWithMultiStreaks(t *testing.T) {
assert.Equal(t, expectedUsers, len(p.Data))
assert.Equal(t, expectedUser1Score, receivedUser1Score)
}

func TestGetLeaderboardWithWinningVote(t *testing.T) {
clearTable("communities")
clearTable("community_users")
clearTable("user_achievements")
clearTable("proposals")
clearTable("proposal_results")
clearTable("votes")
communityId := otu.AddCommunities(1)[0]
winningVoteBonus := 1

proposalId := otu.GenerateWinningVoteAchievement(communityId, "one-address-one-vote")
otu.UpdateProposalEndTime(proposalId, time.Now().UTC())
otu.GetProposalResultsAPI(proposalId)

response := otu.GetCommunityLeaderboardAPI(communityId)
checkResponseCode(t, http.StatusOK, response.Code)

var p test_utils.PaginatedResponseWithLeaderboardUser
json.Unmarshal(response.Body.Bytes(), &p)

winningUserScore := 1 + 1*winningVoteBonus
losingUserScore := 1

receivedWinners := 0
receivedLosers := 0

for _, user := range p.Data {
if user.Score == winningUserScore {
receivedWinners += 1
} else if user.Score == losingUserScore {
receivedLosers += 1
}
}

expectedWinners := 3
expectedLosers := 1

assert.Equal(t, expectedWinners, receivedWinners)
assert.Equal(t, expectedLosers, receivedLosers)
}
60 changes: 40 additions & 20 deletions backend/main/models/community_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ type CommunityUserPayload struct {
}

type UserAchievements = []struct {
Address string
NumVotes int
EarlyVote int
Streak int
Address string
NumVotes int
EarlyVote int
Streak int
WinningVote int
}

type LeaderboardUserPayload struct {
Expand Down Expand Up @@ -111,6 +112,7 @@ func GetCommunityLeaderboard(db *s.Database, communityId, start, count int) ([]L
var leaderboardUsers = []LeaderboardUserPayload{}
var defaultEarlyVoteWeight = 1
var defaultStreakWeight = 1
var defaultWinningVoteWeight = 1

userAchievements, err := getUserAchievements(db, communityId, start, count)

Expand All @@ -121,7 +123,7 @@ func GetCommunityLeaderboard(db *s.Database, communityId, start, count int) ([]L
for _, user := range userAchievements {
var leaderboardUser = LeaderboardUserPayload{}
leaderboardUser.Addr = user.Address
leaderboardUser.Score = user.NumVotes + (user.EarlyVote * defaultEarlyVoteWeight) + (user.Streak * defaultStreakWeight)
leaderboardUser.Score = user.NumVotes + (user.EarlyVote * defaultEarlyVoteWeight) + (user.Streak * defaultStreakWeight) + (user.WinningVote * defaultWinningVoteWeight)
leaderboardUsers = append(leaderboardUsers, leaderboardUser)
}

Expand Down Expand Up @@ -272,26 +274,44 @@ func getUserAchievements(db *s.Database, communityId int, start int, count int)
// their votes and achievements (e.g. early votes, streaks and winning choices)
// Note 1: crosstab is a postgres extension that creates a pivot table.
// Achievements are joined as columns for each user.
// Note 2: Subselect community_id not replaced properly by $1, so has been
// Note 2: Subselects community_id not replaced properly by $1, so has been
// substituted in string first.
sql := fmt.Sprintf(
`
SELECT v.addr as address, count(*) as num_votes,
CASE WHEN a.early_vote is NULL THEN 0 ELSE a.early_vote END as early_vote
FROM votes v
LEFT OUTER JOIN proposals p ON p.id = v.proposal_id
LEFT OUTER JOIN (
SELECT * FROM crosstab(
$$SELECT addr, achievement_type, count(*) FROM user_achievements
WHERE community_id = %d
GROUP BY addr, achievement_type
ORDER BY 1,2$$
) AS ct(address varchar(18), early_vote bigint)
) a ON v.addr = a.address
SELECT v.addr as address, count(*) as num_votes,
COALESCE(a.early_vote, 0) as early_vote,
COALESCE(b.streak, 0) as streak,
COALESCE(c.winning_vote, 0) as winning_vote
FROM votes v
LEFT OUTER JOIN proposals p ON p.id = v.proposal_id
LEFT OUTER JOIN (
SELECT * FROM crosstab(
$$SELECT addr, achievement_type, count(*) FROM user_achievements
WHERE community_id = %d and achievement_type = 'earlyVote'
GROUP BY addr, achievement_type
ORDER BY 1,2$$
) AS ct(address varchar(18), early_vote bigint)
) a ON v.addr = a.address
LEFT OUTER JOIN (
SELECT * FROM crosstab(
$$SELECT addr, achievement_type, count(*) FROM user_achievements
WHERE community_id = %d and achievement_type = 'streak'
GROUP BY addr, achievement_type
ORDER BY 1,2$$
) AS ct(address varchar(18), streak bigint)
) b ON v.addr = b.address
LEFT OUTER JOIN (
SELECT * FROM crosstab(
$$SELECT addr, achievement_type, count(*) FROM user_achievements
WHERE community_id = %d and achievement_type = 'winningVote'
GROUP BY addr, achievement_type
ORDER BY 1,2$$
) AS ct(address varchar(18), winning_vote bigint)
) c ON v.addr = c.address
WHERE p.community_id = $1
GROUP BY v.addr, a.early_vote
GROUP BY v.addr, a.early_vote, b.streak, c.winning_vote
LIMIT $2 OFFSET $3
`, communityId)
`, communityId, communityId, communityId)

err := pgxscan.Select(db.Context, db.Conn, &userAchievements, sql, communityId, count, start)

Expand Down
51 changes: 46 additions & 5 deletions backend/main/models/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -206,7 +207,7 @@ func (v *Vote) GetVoteById(db *s.Database) error {
}

func (v *Vote) CreateVote(db *s.Database) error {
var defaultEarlyVoteLength = 1
var defaultEarlyVoteLength = 2 // in hours

err := createVote(db, v)
if err != nil {
Expand All @@ -221,13 +222,13 @@ func (v *Vote) CreateVote(db *s.Database) error {
isEarlyVote := v.Created_at.Before(proposal.Start_time.Add(time.Hour * time.Duration(defaultEarlyVoteLength)))

if isEarlyVote {
err = addEarlyVoteAchievement(db, v, proposal)
err = AddEarlyVoteAchievement(db, v, proposal)
if err != nil {
return err
}
}

err = addStreakAchievement(db, v, proposal)
err = AddStreakAchievement(db, v, proposal)
return err
}

Expand Down Expand Up @@ -358,7 +359,7 @@ func getProposal(db *s.Database, proposalId int) (Proposal, error) {
return proposal, err
}

func addEarlyVoteAchievement(db *s.Database, v *Vote, p Proposal) error {
func AddEarlyVoteAchievement(db *s.Database, v *Vote, p Proposal) error {

//Unique identifier ensuring there are no duplicate early vote achievements
earlyVoteDetails := fmt.Sprintf("%s:%s:%d:%d", EarlyVote, v.Addr, p.Community_id, v.Proposal_id)
Expand All @@ -378,7 +379,7 @@ func addEarlyVoteAchievement(db *s.Database, v *Vote, p Proposal) error {
return nil
}

func addStreakAchievement(db *s.Database, v *Vote, p Proposal) error {
func AddStreakAchievement(db *s.Database, v *Vote, p Proposal) error {
var defaultStreakLength = 3

votingStreak, err := getUserVotingStreak(db, v.Addr, p.Community_id)
Expand All @@ -392,7 +393,14 @@ func addStreakAchievement(db *s.Database, v *Vote, p Proposal) error {
if vote.Addr != "" {
proposals = append(proposals, vote.Proposal_id)
}


if len(proposals) >= defaultStreakLength && (i == len(votingStreak)-1 || (i < len(votingStreak)-1 && votingStreak[i+1].Addr == "")) {
// ensure proposals always ordered to guarantee no duplicates
sort.Slice(proposals, func(i, j int) bool {
return proposals[i] < proposals[j]
})

//Unique identifier for current streak
currentStreakDetails := fmt.Sprintf("%s:%s:%d:%s", Streak, v.Addr, p.Community_id, strings.Trim(strings.Join(strings.Fields(fmt.Sprint(proposals)), ","), "[]"))

Expand Down Expand Up @@ -420,6 +428,39 @@ func addStreakAchievement(db *s.Database, v *Vote, p Proposal) error {
return nil
}

func AddWinningVoteAchievement(db *s.Database, votes []*VoteWithBalance, p ProposalResults, communityId int) error {
maxVotes := 0
winningChoice := ""
for k, v := range p.Results {
if v > maxVotes {
maxVotes = v
winningChoice = k
}
}
for _, v := range votes {
if v.Choice == winningChoice {
details := fmt.Sprintf("%s:%s:%d:%d", WinningVote, v.Addr, communityId, v.Proposal_id)
err := db.Conn.QueryRow(db.Context,
`
INSERT INTO user_achievements(addr, achievement_type, community_id, proposals, details)
VALUES($1, $2, $3, $4, $5)
ON CONFLICT (details)
DO NOTHING
RETURNING id
`, v.Addr, WinningVote, communityId, []int{v.Proposal_id}, details).Scan(&v.ID)

if checkErrorIgnoreNoRows(err) {
// Row already exists, so we have previously calculated
// winning achievements for this proposal
return nil
} else if err != nil {
return err
}
}
}
return nil
}

func getUserVotingStreak(db *s.Database, addr string, communityId int) ([]VotingStreak, error) {
// Determine if this vote is part of a streak
// Proposals with the user address count as a vote for that proposal
Expand Down
4 changes: 4 additions & 0 deletions backend/main/server/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ func (a *App) getResultsForProposal(w http.ResponseWriter, r *http.Request) {
return
}

if *p.Computed_status == "closed" {
models.AddWinningVoteAchievement(a.DB, votes, proposalResults, p.Community_id)
}

// Send Proposal Results
respondWithJSON(w, http.StatusOK, proposalResults)
}
Expand Down
1 change: 0 additions & 1 deletion backend/main/strategies/token_weighted_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func (s *TokenWeightedDefault) TallyVotes(
votes []*models.VoteWithBalance,
p *models.ProposalResults,
) (models.ProposalResults, error) {

//tally votes
for _, vote := range votes {
if vote.PrimaryAccountBalance != nil {
Expand Down
10 changes: 10 additions & 0 deletions backend/main/test_utils/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ func (otu *OverflowTestUtils) AddActiveProposalsWithStartTimeNow(cId int, count
return retIds
}

func (otu *OverflowTestUtils) UpdateProposalEndTime(pId int, endTime time.Time) {
_, err := otu.A.DB.Conn.Exec(otu.A.DB.Context,
`
UPDATE proposals SET end_time = $2 WHERE id = $1
`, pId, endTime)
if err != nil {
log.Error().Err(err).Msg("update proposal end_time DB err")
}
}

func (otu *OverflowTestUtils) AddLists(cId int, count int) []int {
if count < 1 {
count = 1
Expand Down
14 changes: 14 additions & 0 deletions backend/main/test_utils/leaderboard_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ func (otu *OverflowTestUtils) GenerateMultiStreakAchievements(communityId int, s
}
}

func (otu *OverflowTestUtils) GenerateWinningVoteAchievement(communityId int, strategy string) int {
proposalIds, _ := otu.AddProposalsForStrategy(communityId, strategy, 1)
proposalId := proposalIds[0]
winningChoice := "a"
losingChoice := "b"

otu.CreateVoteAPI(proposalId, otu.GenerateValidVotePayload("user1", proposalId, losingChoice))
otu.CreateVoteAPI(proposalId, otu.GenerateValidVotePayload("user2", proposalId, winningChoice))
otu.CreateVoteAPI(proposalId, otu.GenerateValidVotePayload("user3", proposalId, winningChoice))
otu.CreateVoteAPI(proposalId, otu.GenerateValidVotePayload("user4", proposalId, winningChoice))

return proposalId
}

func max(s []int) int {
var m int
for i, v := range s {
Expand Down
15 changes: 15 additions & 0 deletions backend/main/test_utils/proposal_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ func (otu *OverflowTestUtils) GenerateCancelProposalStruct(
return &payload
}

func (otu *OverflowTestUtils) GenerateClosedProposalStruct(
signer string,
proposalId int,
) *models.UpdateProposalRequestPayload {
payload := models.UpdateProposalRequestPayload{Status: "closed"}
timestamp := fmt.Sprint(time.Now().UnixNano() / int64(time.Millisecond))
compositeSignatures := otu.GenerateCompositeSignatures(signer, timestamp)
account, _ := otu.O.State.Accounts().ByName(fmt.Sprintf("emulator-%s", signer))
payload.Signing_addr = fmt.Sprintf("0x%s", account.Address().String())
payload.Timestamp = timestamp
payload.Composite_signatures = compositeSignatures

return &payload
}

func (otu *OverflowTestUtils) GenerateProposalPayload(signer string, proposal *models.Proposal) *models.Proposal {
timestamp := fmt.Sprint(time.Now().UnixNano() / int64(time.Millisecond))
compositeSignatures := otu.GenerateCompositeSignatures(signer, timestamp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE user_achievements RENAME TO community_users_achievements;
ALTER TABLE community_users_achievements DROP COLUMN details;

Loading