code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/games.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package sqlstore 17 18 import ( 19 "context" 20 "encoding/hex" 21 "fmt" 22 "sort" 23 "strings" 24 "time" 25 26 "code.vegaprotocol.io/vega/datanode/entities" 27 "code.vegaprotocol.io/vega/libs/num" 28 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 29 "code.vegaprotocol.io/vega/protos/vega" 30 31 "github.com/georgysavva/scany/pgxscan" 32 "github.com/shopspring/decimal" 33 ) 34 35 type Games struct { 36 *ConnectionSource 37 } 38 39 var gameOrdering = TableOrdering{ 40 ColumnOrdering{Name: "epoch_id", Sorting: ASC}, 41 ColumnOrdering{Name: "game_id", Sorting: DESC}, 42 } 43 44 func NewGames(connectionSource *ConnectionSource) *Games { 45 return &Games{ 46 ConnectionSource: connectionSource, 47 } 48 } 49 50 type GameReward struct { 51 PartyID entities.PartyID 52 AssetID entities.AssetID 53 MarketID entities.MarketID 54 EpochID int64 55 Amount decimal.Decimal 56 QuantumAmount decimal.Decimal 57 PercentOfTotal float64 58 RewardType string 59 Timestamp time.Time 60 TxHash entities.TxHash 61 VegaTime time.Time 62 SeqNum uint64 63 LockedUntilEpochID int64 64 GameID []byte 65 DispatchStrategy vega.DispatchStrategy 66 TeamID entities.TeamID 67 MemberRank *int64 68 TeamRank *int64 69 TotalRewards num.Decimal 70 TotalRewardsQuantum num.Decimal 71 TeamTotalRewards *num.Decimal 72 TeamTotalRewardsQuantum *num.Decimal 73 EntityScope string 74 } 75 76 func (g *Games) ListGames(ctx context.Context, gameID *string, entityScope *vega.EntityScope, epochFrom, epochTo *uint64, 77 teamID *entities.TeamID, partyID *entities.PartyID, pagination entities.CursorPagination, 78 ) ([]entities.Game, entities.PageInfo, error) { 79 var pageInfo entities.PageInfo 80 81 var gameRewards []GameReward 82 83 // because we have to build the games data from the rewards data, paging backwards adds more complexity 84 // therefore we aren't going to support it for now as the games data API is high priority 85 if pagination.HasBackward() { 86 return nil, pageInfo, fmt.Errorf("backward pagination is not currently supported") 87 } 88 89 query, args, err := g.buildGamesQuery(gameID, entityScope, epochFrom, epochTo, teamID, partyID, pagination) 90 if err != nil { 91 return nil, pageInfo, err 92 } 93 94 if err := pgxscan.Select(ctx, g.ConnectionSource, &gameRewards, query, args...); err != nil { 95 return nil, pageInfo, err 96 } 97 98 games, err := parseGameRewards(gameRewards) 99 if err != nil { 100 return nil, pageInfo, err 101 } 102 103 games, pageInfo = entities.PageEntities[*v2.GameEdge](games, pagination) 104 105 return games, pageInfo, nil 106 } 107 108 func (g *Games) parseEpochs(from, to *uint64) (uint64, uint64) { 109 var eFrom, eTo uint64 110 if from != nil || to != nil { 111 // no more than 30 epochs for performance sake 112 if from != nil && to == nil { 113 eFrom, eTo = *from, *from+30-1 114 } else if from == nil && to != nil { 115 eTo, eFrom = *to, *to-30+1 116 } else if from != nil && to != nil { 117 eFrom, eTo = *from, *to 118 if eTo-eFrom > 30 { 119 eFrom = eTo - 30 + 1 120 } 121 } 122 } 123 return eFrom, eTo 124 } 125 126 func (g *Games) buildPagingQuery(selectTable string, gameID *string, entityScope *vega.EntityScope, epochFrom, epochTo uint64, 127 teamID *entities.TeamID, partyID *entities.PartyID, pagination entities.CursorPagination, 128 ) (string, []interface{}, error) { 129 selectQuery := fmt.Sprintf(`select distinct game_id, epoch_id from %s`, selectTable) 130 var where []string 131 var args []interface{} 132 133 if epochFrom > 0 && epochTo > 0 { 134 where = append(where, fmt.Sprintf("epoch_id >= %s AND epoch_id <= %s", 135 nextBindVar(&args, epochFrom), nextBindVar(&args, epochTo))) 136 } 137 138 if gameID != nil { 139 where = append(where, fmt.Sprintf("game_id = %s", nextBindVar(&args, entities.GameID(*gameID)))) 140 } 141 142 if entityScope != nil { 143 where = append(where, fmt.Sprintf("entity_scope = %s", nextBindVar(&args, entityScope.String()))) 144 145 // only add the teams filter if the entity scope is teams or not specified 146 if *entityScope == vega.EntityScope_ENTITY_SCOPE_TEAMS && teamID != nil { 147 where = append(where, fmt.Sprintf("team_id = %s", nextBindVar(&args, teamID))) 148 } 149 } else if entityScope == nil && teamID != nil { 150 where = append(where, fmt.Sprintf("team_id = %s", nextBindVar(&args, teamID))) 151 } 152 153 // We should be able to filter by party regardless of the entity scope 154 if partyID != nil { 155 where = append(where, fmt.Sprintf("party_id = %s", nextBindVar(&args, partyID))) 156 } 157 158 whereClause := "" 159 if len(where) > 0 { 160 whereClause = "WHERE " + strings.Join(where, " AND ") 161 } 162 163 query := fmt.Sprintf("%s %s", selectQuery, whereClause) 164 return PaginateQuery[entities.GameCursor](query, args, gameOrdering, pagination) 165 } 166 167 func (g *Games) buildGamesQuery(gameID *string, entityScope *vega.EntityScope, epochFrom, epochTo *uint64, 168 teamID *entities.TeamID, partyID *entities.PartyID, pagination entities.CursorPagination, 169 ) (string, []interface{}, error) { 170 // Games are intrinsically created by a recurring transfer with a game ID 171 // Rewards are paid out to participants of a game and the game ID is recorded on the reward 172 // We need to query the rewards and build the games data from that. 173 // If we page on the reward data, we may not have a complete data set for the game. Therefore we need to only page/filter on the distinct game IDs per epoch 174 // and then use that data to query the corresponding rewards data we need for the API to return. 175 176 eFrom, eTo := g.parseEpochs(epochFrom, epochTo) 177 // The select table query determines if we should just be querying the games data for the most current epoch or all epochs 178 selectTable := g.getSelectTable(eFrom, eTo) 179 // The page query determines which games/epochs should be included in the result set for pagination 180 // For example, if we have 100 games, and we want to page on the first 10, we would need to know which games to include rewards for 181 // The number of rewards we may get back in order to build the data will be much more than just 10 records. 182 pageQuery, args, err := g.buildPagingQuery(selectTable, gameID, entityScope, eFrom, eTo, teamID, partyID, pagination) 183 if err != nil { 184 return "", nil, err 185 } 186 187 query := fmt.Sprintf("select s.* from %s s join (%s) as p on s.game_id = p.game_id and s.epoch_id = p.epoch_id order by s.epoch_id desc, s.game_id", selectTable, pageQuery) 188 189 return query, args, nil 190 } 191 192 func (g *Games) getSelectTable(from, to uint64) string { 193 if from == 0 && to == 0 { 194 return `game_stats_current` 195 } 196 return `game_stats` 197 } 198 199 func parseGameRewards(rewards []GameReward) ([]entities.Game, error) { 200 if len(rewards) <= 0 { 201 return []entities.Game{}, nil 202 } 203 204 type gameKey struct { 205 EpochID uint64 206 GameID entities.GameID 207 } 208 games := make(map[gameKey]entities.Game) 209 lastGameID := entities.GameID("") 210 lastEpochID := int64(0) 211 participants := uint64(0) 212 213 gameIndividuals := make(map[gameKey][]entities.GameEntity) 214 teamMembers := make(map[gameKey]map[entities.TeamID][]*entities.IndividualGameEntity) 215 teamRanks := make(map[gameKey]map[entities.TeamID]uint64) 216 217 var game entities.Game 218 var gk gameKey 219 220 // first go through all the rewards and build the participation stats 221 // if the reward is for a team participant, i.e. there is a team ID then the participant will be added to the teamMembers map 222 // otherwise we add it to the gameIndividuals map 223 for i := range rewards { 224 gID := hex.EncodeToString(rewards[i].GameID) 225 currentGameID := entities.GameID(gID) 226 currentEpochID := rewards[i].EpochID 227 gk = gameKey{ 228 EpochID: uint64(currentEpochID), 229 GameID: currentGameID, 230 } 231 232 if currentGameID != lastGameID || currentEpochID != lastEpochID { 233 // add the game to the map of games 234 lastKey := gameKey{ 235 EpochID: uint64(lastEpochID), 236 GameID: lastGameID, 237 } 238 if lastGameID != "" && lastEpochID != 0 { 239 game.Participants = participants 240 games[lastKey] = game 241 } 242 243 game = entities.Game{ 244 ID: currentGameID, 245 Epoch: uint64(currentEpochID), 246 Participants: participants, 247 Entities: []entities.GameEntity{}, 248 RewardAssetID: rewards[i].AssetID, 249 } 250 251 lastGameID = currentGameID 252 lastEpochID = currentEpochID 253 participants = 0 254 games[gk] = game 255 } 256 257 rewardEarned, _ := num.UintFromDecimal(rewards[i].Amount) 258 totalRewardsEarned, _ := num.UintFromDecimal(rewards[i].TotalRewards) 259 rewardEarnedQuantum, _ := num.UintFromDecimal(rewards[i].QuantumAmount) 260 totalRewardsEarnedQuantum, _ := num.UintFromDecimal(rewards[i].TotalRewardsQuantum) 261 262 var rank uint64 263 if rewards[i].MemberRank != nil { 264 rank = uint64(*rewards[i].MemberRank) 265 } 266 267 individual := entities.IndividualGameEntity{ 268 Individual: rewards[i].PartyID.String(), 269 Rank: rank, 270 Volume: num.DecimalZero(), 271 RewardMetric: rewards[i].DispatchStrategy.Metric, 272 RewardEarned: rewardEarned, 273 TotalRewardsEarned: totalRewardsEarned, 274 RewardEarnedQuantum: rewardEarnedQuantum, 275 TotalRewardsEarnedQuantum: totalRewardsEarnedQuantum, 276 } 277 278 if rewards[i].TeamID != "" { 279 currentTeamID := rewards[i].TeamID 280 if teamMembers[gk] == nil { 281 teamMembers[gk] = make(map[entities.TeamID][]*entities.IndividualGameEntity) 282 } 283 teamMembers[gk][currentTeamID] = append(teamMembers[gk][currentTeamID], &individual) 284 if rewards[i].TeamRank == nil { 285 return nil, fmt.Errorf("team rank is nil for team %s", currentTeamID) 286 } 287 288 if teamRanks[gk] == nil { 289 teamRanks[gk] = make(map[entities.TeamID]uint64) 290 } 291 292 teamRanks[gk][currentTeamID] = uint64(*rewards[i].TeamRank) 293 } else { 294 gameIndividuals[gk] = append(gameIndividuals[gk], &individual) 295 } 296 participants++ 297 } 298 299 game.Participants = participants 300 games[gk] = game 301 302 results := make([]entities.Game, 0, len(games)) 303 // now that we have the participation involvement, we can use that to build the game entities for each game. 304 for key, game := range games { 305 if teamMembers[key] != nil { 306 for teamID, individuals := range teamMembers[key] { 307 sort.Slice(individuals, func(i, j int) bool { 308 return individuals[i].Rank < individuals[j].Rank || (individuals[i].Rank == individuals[j].Rank && individuals[i].Individual < individuals[j].Individual) 309 }) 310 team := entities.TeamGameParticipation{ 311 TeamID: teamID, 312 MembersParticipating: individuals, 313 } 314 315 teamVolume := num.DecimalZero() 316 teamRewardEarned := num.NewUint(0) 317 teamTotalRewardsEarned := num.NewUint(0) 318 teamRewardEarnedQuantum := num.NewUint(0) 319 teamTotalRewardsEarnedQuantum := num.NewUint(0) 320 rewardMetric := vega.DispatchMetric_DISPATCH_METRIC_UNSPECIFIED 321 for _, individual := range individuals { 322 if rewardMetric == vega.DispatchMetric_DISPATCH_METRIC_UNSPECIFIED { 323 rewardMetric = individual.RewardMetric 324 } 325 teamVolume = teamVolume.Add(individual.Volume) 326 teamRewardEarned = teamRewardEarned.Add(teamRewardEarned, individual.RewardEarned) 327 teamTotalRewardsEarned = teamTotalRewardsEarned.Add(teamTotalRewardsEarned, individual.TotalRewardsEarned) 328 teamRewardEarnedQuantum = teamRewardEarnedQuantum.Add(teamRewardEarnedQuantum, individual.RewardEarnedQuantum) 329 teamTotalRewardsEarnedQuantum = teamTotalRewardsEarnedQuantum.Add(teamTotalRewardsEarnedQuantum, individual.TotalRewardsEarnedQuantum) 330 } 331 game.Entities = append(game.Entities, &entities.TeamGameEntity{ 332 Team: team, 333 Rank: teamRanks[key][teamID], 334 Volume: teamVolume, 335 RewardMetric: rewardMetric, 336 RewardEarned: teamRewardEarned, 337 TotalRewardsEarned: teamTotalRewardsEarned, 338 RewardEarnedQuantum: teamRewardEarnedQuantum, 339 TotalRewardsEarnedQuantum: teamTotalRewardsEarnedQuantum, 340 }) 341 } 342 sort.Slice(game.Entities, func(i, j int) bool { 343 return game.Entities[i].(*entities.TeamGameEntity).Rank < game.Entities[j].(*entities.TeamGameEntity).Rank || 344 (game.Entities[i].(*entities.TeamGameEntity).Rank == game.Entities[j].(*entities.TeamGameEntity).Rank && 345 game.Entities[i].(*entities.TeamGameEntity).Team.TeamID < game.Entities[j].(*entities.TeamGameEntity).Team.TeamID) 346 }) 347 } 348 if gameIndividuals[key] != nil { 349 game.Entities = append(game.Entities, gameIndividuals[key]...) 350 sort.Slice(game.Entities, func(i, j int) bool { 351 return game.Entities[i].(*entities.IndividualGameEntity).Rank < game.Entities[j].(*entities.IndividualGameEntity).Rank || 352 (game.Entities[i].(*entities.IndividualGameEntity).Rank == game.Entities[j].(*entities.IndividualGameEntity).Rank && 353 game.Entities[i].(*entities.IndividualGameEntity).Individual < game.Entities[j].(*entities.IndividualGameEntity).Individual) 354 }) 355 } 356 results = append(results, game) 357 } 358 sort.Slice(results, func(i, j int) bool { 359 return results[i].Epoch > results[j].Epoch || 360 (results[i].Epoch == results[j].Epoch && results[i].ID < results[j].ID) 361 }) 362 return results, nil 363 }