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  }