code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/teams.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  	"fmt"
    21  	"strings"
    22  
    23  	"code.vegaprotocol.io/vega/datanode/entities"
    24  	"code.vegaprotocol.io/vega/datanode/metrics"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    27  	eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1"
    28  
    29  	"github.com/georgysavva/scany/pgxscan"
    30  )
    31  
    32  const (
    33  	listTeamsStatsQuery = `WITH
    34    -- This CTE retrieves the all teams statistics reported for the last N epochs.
    35    eligible_stats AS (
    36      SELECT *
    37      FROM teams_stats
    38      WHERE at_epoch > (
    39        SELECT MAX(id) - $1
    40        FROM epochs
    41      ) %s
    42    ),
    43    team_numbers AS (
    44      SELECT t.team_id,
    45             SUM(total_quantum_reward) AS total_quantum_rewards,
    46             JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_reward) ORDER BY at_epoch, total_quantum_reward) AS quantum_rewards,
    47             SUM(total_quantum_volume) AS total_quantum_volumes,
    48             JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_volume) ORDER BY at_epoch, total_quantum_volume) AS quantum_volumes
    49      FROM eligible_stats t
    50      GROUP BY
    51        t.team_id
    52    ),
    53    team_games AS (
    54      SELECT team_id,
    55             COALESCE(ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT game_played), NULL), 1), 0) AS count,
    56             COALESCE(JSONB_AGG(DISTINCT game_played::BYTEA ORDER BY game_played::BYTEA)
    57                      FILTER (WHERE game_played <> 'null' ), '[]'::JSONB) AS LIST
    58      FROM eligible_stats stats
    59        LEFT JOIN LATERAL JSONB_OBJECT_KEYS(stats.games_played) AS game_played ON TRUE
    60      GROUP BY
    61        team_id
    62    )
    63  SELECT tn.team_id AS team_id,
    64         tn.total_quantum_rewards AS total_quantum_rewards,
    65         tn.quantum_rewards AS quantum_rewards,
    66         tn.total_quantum_volumes AS total_quantum_volumes,
    67         tn.quantum_volumes AS quantum_volumes,
    68         mg.list AS games_played,
    69         mg.count AS total_games_played
    70  FROM team_numbers tn
    71    LEFT JOIN team_games mg ON tn.team_id = mg.team_id
    72  `
    73  
    74  	listTeamMembersStatsQuery = `WITH
    75    -- This CTE retrieves the all teams statistics reported for the last N epochs.
    76    eligible_stats AS (
    77      SELECT *
    78      FROM teams_stats
    79      WHERE at_epoch > (
    80        SELECT MAX(id) - $1
    81        FROM epochs
    82      ) AND team_id = $2 %s
    83    ),
    84    members_numbers AS (
    85      SELECT team_id,
    86             party_id,
    87             SUM(total_quantum_reward) AS total_quantum_rewards,
    88             JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_reward) ORDER BY at_epoch, total_quantum_reward) AS quantum_rewards,
    89             SUM(total_quantum_volume) AS total_quantum_volumes,
    90             JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_volume) ORDER BY at_epoch, total_quantum_volume) AS quantum_volumes
    91      FROM eligible_stats
    92      GROUP BY
    93        team_id,
    94        party_id
    95    ),
    96    members_games AS (
    97      SELECT team_id,
    98             party_id,
    99             COALESCE(ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT game_played), NULL), 1), 0) AS count,
   100             COALESCE(JSONB_AGG(DISTINCT game_played::BYTEA ORDER BY game_played::BYTEA)
   101                      FILTER ( WHERE game_played <> 'null' ), '[]'::JSONB) AS list
   102      FROM eligible_stats stats
   103        LEFT JOIN LATERAL JSONB_OBJECT_KEYS(stats.games_played) AS game_played ON TRUE
   104      GROUP BY
   105        team_id,
   106        party_id
   107    )
   108  SELECT mn.party_id AS party_id,
   109         mn.total_quantum_rewards AS total_quantum_rewards,
   110         mn.quantum_rewards AS quantum_rewards,
   111         mn.total_quantum_volumes AS total_quantum_volumes,
   112         mn.quantum_volumes AS quantum_volumes,
   113         mg.list AS games_played,
   114         mg.count AS total_games_played
   115  FROM members_numbers mn
   116    LEFT JOIN members_games mg ON mn.team_id = mg.team_id AND mn.party_id = mg.party_id`
   117  
   118  	upsertTeamsStats = `INSERT INTO teams_stats(team_id, party_id, at_epoch, total_quantum_volume, total_quantum_reward, games_played)
   119  VALUES
   120    %s
   121  ON CONFLICT (team_id, party_id, at_epoch) DO UPDATE
   122    SET total_quantum_volume = excluded.total_quantum_volume
   123  	`
   124  )
   125  
   126  type (
   127  	Teams struct {
   128  		*ConnectionSource
   129  	}
   130  
   131  	ListTeamsStatisticsFilters struct {
   132  		TeamID            *entities.TeamID
   133  		AggregationEpochs uint64
   134  	}
   135  
   136  	ListTeamMembersStatisticsFilters struct {
   137  		TeamID            entities.TeamID
   138  		PartyID           *entities.PartyID
   139  		AggregationEpochs uint64
   140  	}
   141  )
   142  
   143  var (
   144  	teamsOrdering = TableOrdering{
   145  		ColumnOrdering{Name: "created_at", Sorting: ASC},
   146  	}
   147  	teamsStatsOrdering = TableOrdering{
   148  		ColumnOrdering{Name: "team_id", Sorting: ASC},
   149  	}
   150  	teamMembersStatsOrdering = TableOrdering{
   151  		ColumnOrdering{Name: "party_id", Sorting: ASC},
   152  	}
   153  	refereesOrdering = TableOrdering{
   154  		ColumnOrdering{Name: "party_id", Sorting: ASC},
   155  	}
   156  	refereeHistoryOrdering = TableOrdering{
   157  		ColumnOrdering{Name: "joined_at_epoch", Sorting: ASC},
   158  	}
   159  )
   160  
   161  func NewTeams(connectionSource *ConnectionSource) *Teams {
   162  	return &Teams{
   163  		ConnectionSource: connectionSource,
   164  	}
   165  }
   166  
   167  func (t *Teams) AddTeam(ctx context.Context, team *entities.Team) error {
   168  	defer metrics.StartSQLQuery("Teams", "AddTeam")()
   169  
   170  	if team.AllowList == nil {
   171  		team.AllowList = []string{}
   172  	}
   173  
   174  	if _, err := t.Exec(
   175  		ctx,
   176  		"INSERT INTO teams(id, referrer, name, team_url, avatar_url, closed, allow_list, created_at, created_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
   177  		team.ID,
   178  		team.Referrer,
   179  		team.Name,
   180  		team.TeamURL,
   181  		team.AvatarURL,
   182  		team.Closed,
   183  		team.AllowList,
   184  		team.CreatedAt,
   185  		team.CreatedAtEpoch,
   186  		team.VegaTime,
   187  	); err != nil {
   188  		return err
   189  	}
   190  
   191  	// in case the party already was in a team?
   192  	_, _ = t.Exec(
   193  		ctx,
   194  		"DELETE FROM team_members WHERE party_id = $1",
   195  		team.Referrer,
   196  	)
   197  
   198  	if _, err := t.Exec(
   199  		ctx,
   200  		"INSERT INTO team_members(team_id, party_id, joined_at_epoch, joined_at, vega_time) VALUES ($1, $2, $3, $4, $5)",
   201  		team.ID,
   202  		team.Referrer,
   203  		team.CreatedAtEpoch,
   204  		team.CreatedAt,
   205  		team.VegaTime,
   206  	); err != nil {
   207  		return err
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  func (t *Teams) UpdateTeam(ctx context.Context, team *entities.TeamUpdated) error {
   214  	defer metrics.StartSQLQuery("Teams", "UpdateTeam")()
   215  
   216  	if team.AllowList == nil {
   217  		team.AllowList = []string{}
   218  	}
   219  
   220  	ct, err := t.Exec(ctx,
   221  		`UPDATE teams
   222          SET name = $1,
   223              team_url = $2,
   224              avatar_url = $3,
   225              closed = $4,
   226              allow_list = $5
   227          WHERE id = $6`,
   228  		team.Name,
   229  		team.TeamURL,
   230  		team.AvatarURL,
   231  		team.Closed,
   232  		team.AllowList,
   233  		team.ID,
   234  	)
   235  
   236  	if ct.RowsAffected() == 0 {
   237  		return fmt.Errorf("could not update team with id %s", team.ID)
   238  	}
   239  	return err
   240  }
   241  
   242  func (t *Teams) RefereeJoinedTeam(ctx context.Context, referee *entities.TeamMember) error {
   243  	defer metrics.StartSQLQuery("Teams", "RefereeJoinedTeam")()
   244  	_, err := t.Exec(ctx,
   245  		`INSERT INTO team_members(team_id, party_id, joined_at, joined_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5)`,
   246  		referee.TeamID,
   247  		referee.PartyID,
   248  		referee.JoinedAt,
   249  		referee.JoinedAtEpoch,
   250  		referee.VegaTime,
   251  	)
   252  
   253  	return err
   254  }
   255  
   256  func (t *Teams) RefereeSwitchedTeam(ctx context.Context, referee *entities.RefereeTeamSwitch) error {
   257  	defer metrics.StartSQLQuery("Teams", "RefereeSwitchedTeam")()
   258  
   259  	// in case the party was removed from the team owner from a team
   260  	if len(referee.ToTeamID) <= 0 {
   261  		_, err := t.Exec(
   262  			ctx,
   263  			"DELETE FROM team_members WHERE party_id = $1",
   264  			referee.PartyID,
   265  		)
   266  
   267  		return err
   268  	}
   269  
   270  	// normal path, team_members just being updated.
   271  	_, err := t.Exec(ctx,
   272  		`INSERT INTO team_members(team_id, party_id, joined_at, joined_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5)`,
   273  		referee.ToTeamID,
   274  		referee.PartyID,
   275  		referee.SwitchedAt,
   276  		referee.SwitchedAtEpoch,
   277  		referee.VegaTime,
   278  	)
   279  
   280  	return err
   281  }
   282  
   283  func (t *Teams) TeamsStatsUpdated(ctx context.Context, evt *eventspb.TeamsStatsUpdated) error {
   284  	defer metrics.StartSQLQuery("Teams", "TeamsStatsUpdated")()
   285  
   286  	var args []interface{}
   287  
   288  	values := []string{}
   289  	for _, teamStats := range evt.Stats {
   290  		for _, memberStats := range teamStats.MembersStats {
   291  			notionalVolume, hasErr := num.UintFromString(memberStats.NotionalVolume, 10)
   292  			if hasErr {
   293  				notionalVolume = num.UintZero()
   294  			}
   295  
   296  			values = append(values, fmt.Sprintf("(%s, %s, %s, %s, 0, '{}'::JSONB)",
   297  				nextBindVar(&args, entities.TeamID(teamStats.TeamId)),
   298  				nextBindVar(&args, entities.PartyID(memberStats.PartyId)),
   299  				nextBindVar(&args, evt.AtEpoch),
   300  				nextBindVar(&args, notionalVolume)),
   301  			)
   302  		}
   303  	}
   304  
   305  	if len(values) == 0 {
   306  		return nil
   307  	}
   308  
   309  	query := fmt.Sprintf(upsertTeamsStats, strings.Join(values, ","))
   310  	_, err := t.Exec(ctx, query, args...)
   311  	if err != nil {
   312  		return fmt.Errorf("could not insert team stats update: %w", err)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  func (t *Teams) GetTeam(ctx context.Context, teamID entities.TeamID, partyID entities.PartyID) (*entities.Team, error) {
   319  	defer metrics.StartSQLQuery("Teams", "GetTeam")()
   320  
   321  	var team entities.Team
   322  
   323  	if teamID == "" && partyID == "" {
   324  		return nil, fmt.Errorf("either teamID or partyID must be provided")
   325  	}
   326  
   327  	var args []interface{}
   328  
   329  	query := `WITH
   330    members_stats AS (
   331      SELECT team_id, COUNT(DISTINCT party_id) AS total_members
   332      FROM current_team_members
   333      GROUP BY
   334        team_id
   335    )
   336  SELECT teams.*, members_stats.total_members
   337  FROM teams
   338    LEFT JOIN members_stats on teams.id = members_stats.team_id %s`
   339  
   340  	var where string
   341  	if teamID != "" {
   342  		where = fmt.Sprintf("WHERE teams.id = %s", nextBindVar(&args, teamID))
   343  	} else if partyID != "" {
   344  		where = fmt.Sprintf("INNER JOIN current_team_members ON current_team_members.party_id = %s AND teams.id = current_team_members.team_id", nextBindVar(&args, partyID))
   345  	}
   346  
   347  	query = fmt.Sprintf(query, where)
   348  
   349  	if err := pgxscan.Get(ctx, t.ConnectionSource, &team, query, args...); err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	return &team, nil
   354  }
   355  
   356  func (t *Teams) ListTeams(ctx context.Context, pagination entities.CursorPagination) ([]entities.Team, entities.PageInfo, error) {
   357  	defer metrics.StartSQLQuery("Teams", "ListTeams")()
   358  
   359  	var (
   360  		teams    []entities.Team
   361  		args     []interface{}
   362  		pageInfo entities.PageInfo
   363  	)
   364  
   365  	query := `WITH
   366    members_stats AS (
   367      SELECT team_id, COUNT(DISTINCT party_id) AS total_members
   368      FROM current_team_members
   369      GROUP BY
   370        team_id
   371    )
   372  SELECT teams.*, members_stats.total_members
   373  FROM teams
   374    LEFT JOIN members_stats on teams.id = members_stats.team_id`
   375  	query, args, err := PaginateQuery[entities.TeamCursor](query, args, teamsOrdering, pagination)
   376  	if err != nil {
   377  		return nil, pageInfo, err
   378  	}
   379  
   380  	if err := pgxscan.Select(ctx, t.ConnectionSource, &teams, query, args...); err != nil {
   381  		return nil, pageInfo, err
   382  	}
   383  
   384  	teams, pageInfo = entities.PageEntities[*v2.TeamEdge](teams, pagination)
   385  
   386  	return teams, pageInfo, nil
   387  }
   388  
   389  func (t *Teams) ListTeamsStatistics(ctx context.Context, pagination entities.CursorPagination, filters ListTeamsStatisticsFilters) ([]entities.TeamsStatistics, entities.PageInfo, error) {
   390  	defer metrics.StartSQLQuery("Teams", "ListTeamsStatistics")()
   391  
   392  	var (
   393  		teamsStats []entities.TeamsStatistics
   394  		pageInfo   entities.PageInfo
   395  	)
   396  
   397  	query := listTeamsStatsQuery
   398  	args := []any{filters.AggregationEpochs}
   399  
   400  	if filters.TeamID != nil {
   401  		query = fmt.Sprintf(query, fmt.Sprintf(`AND team_id = %s`, nextBindVar(&args, *filters.TeamID)))
   402  	} else {
   403  		query = fmt.Sprintf(query, "")
   404  	}
   405  
   406  	query, args, err := PaginateQuery[entities.TeamsStatisticsCursor](query, args, teamsStatsOrdering, pagination)
   407  	if err != nil {
   408  		return nil, pageInfo, err
   409  	}
   410  
   411  	if err := pgxscan.Select(ctx, t.ConnectionSource, &teamsStats, query, args...); err != nil {
   412  		return nil, pageInfo, err
   413  	}
   414  
   415  	teamsStats, pageInfo = entities.PageEntities[*v2.TeamStatisticsEdge](teamsStats, pagination)
   416  
   417  	// Deserializing the GameID array as a PostgreSQL array is not correctly
   418  	// interpreted by the scanny library. So, we have to use the JSONB array which
   419  	// convert the bytea as strings. This leaves the prefix `\\x` on the game ID.
   420  	// As a result, we have to manually clean up of the ID.
   421  	for i := range teamsStats {
   422  		for j := range teamsStats[i].GamesPlayed {
   423  			teamsStats[i].GamesPlayed[j] = entities.GameID(strings.TrimLeft(teamsStats[i].GamesPlayed[j].String(), "\\x"))
   424  		}
   425  	}
   426  
   427  	return teamsStats, pageInfo, nil
   428  }
   429  
   430  func (t *Teams) ListTeamMembersStatistics(ctx context.Context, pagination entities.CursorPagination, filters ListTeamMembersStatisticsFilters) ([]entities.TeamMembersStatistics, entities.PageInfo, error) {
   431  	defer metrics.StartSQLQuery("Teams", "ListTeamMembersStatistics")()
   432  
   433  	var (
   434  		membersStats []entities.TeamMembersStatistics
   435  		pageInfo     entities.PageInfo
   436  	)
   437  
   438  	query := listTeamMembersStatsQuery
   439  	args := []any{filters.AggregationEpochs, filters.TeamID}
   440  
   441  	if filters.PartyID != nil {
   442  		query = fmt.Sprintf(query, fmt.Sprintf(`AND party_id = %s`, nextBindVar(&args, *filters.PartyID)))
   443  	} else {
   444  		query = fmt.Sprintf(query, "")
   445  	}
   446  
   447  	query, args, err := PaginateQuery[entities.TeamMemberStatisticsCursor](query, args, teamMembersStatsOrdering, pagination)
   448  	if err != nil {
   449  		return nil, pageInfo, err
   450  	}
   451  
   452  	if err := pgxscan.Select(ctx, t.ConnectionSource, &membersStats, query, args...); err != nil {
   453  		return nil, pageInfo, err
   454  	}
   455  
   456  	membersStats, pageInfo = entities.PageEntities[*v2.TeamMemberStatisticsEdge](membersStats, pagination)
   457  
   458  	// Deserializing the GameID array as a PostgreSQL array is not correctly
   459  	// interpreted by the scanny library. So, we have to use the JSONB array which
   460  	// convert the bytea as strings. This leaves the prefix `\\x` on the game ID.
   461  	// As a result, we have to manually clean up of the ID.
   462  	for i := range membersStats {
   463  		for j := range membersStats[i].GamesPlayed {
   464  			membersStats[i].GamesPlayed[j] = entities.GameID(strings.TrimLeft(membersStats[i].GamesPlayed[j].String(), "\\x"))
   465  		}
   466  	}
   467  
   468  	return membersStats, pageInfo, nil
   469  }
   470  
   471  func (t *Teams) ListReferees(ctx context.Context, teamID entities.TeamID, pagination entities.CursorPagination) ([]entities.TeamMember, entities.PageInfo, error) {
   472  	defer metrics.StartSQLQuery("Teams", "ListReferees")()
   473  	var (
   474  		referees []entities.TeamMember
   475  		args     []interface{}
   476  		pageInfo entities.PageInfo
   477  	)
   478  
   479  	if teamID == "" {
   480  		return nil, pageInfo, fmt.Errorf("teamID must be provided")
   481  	}
   482  
   483  	query := `SELECT ctm.*
   484  	FROM current_team_members ctm
   485      LEFT JOIN teams t ON t.id = ctm.team_id
   486  	 WHERE ctm.party_id != t.referrer AND ctm.team_id = %s`
   487  
   488  	query = fmt.Sprintf(query, nextBindVar(&args, teamID))
   489  
   490  	query, args, err := PaginateQuery[entities.RefereeCursor](query, args, refereesOrdering, pagination)
   491  	if err != nil {
   492  		return nil, pageInfo, err
   493  	}
   494  
   495  	if err := pgxscan.Select(ctx, t.ConnectionSource, &referees, query, args...); err != nil {
   496  		return nil, pageInfo, err
   497  	}
   498  
   499  	referees, pageInfo = entities.PageEntities[*v2.TeamRefereeEdge](referees, pagination)
   500  
   501  	return referees, pageInfo, nil
   502  }
   503  
   504  func (t *Teams) ListRefereeHistory(ctx context.Context, referee entities.PartyID, pagination entities.CursorPagination) ([]entities.TeamMemberHistory, entities.PageInfo, error) {
   505  	defer metrics.StartSQLQuery("Teams", "ListRefereeHistory")()
   506  	var (
   507  		referees []entities.TeamMemberHistory
   508  		args     []interface{}
   509  		pageInfo entities.PageInfo
   510  	)
   511  
   512  	if referee == "" {
   513  		return nil, pageInfo, fmt.Errorf("referee must be provided")
   514  	}
   515  
   516  	query := fmt.Sprintf(`SELECT team_id, joined_at_epoch, joined_at FROM team_members WHERE party_id = %s`, nextBindVar(&args, referee))
   517  
   518  	query, args, err := PaginateQuery[entities.RefereeHistoryCursor](query, args, refereeHistoryOrdering, pagination)
   519  	if err != nil {
   520  		return nil, pageInfo, err
   521  	}
   522  
   523  	if err := pgxscan.Select(ctx, t.ConnectionSource, &referees, query, args...); err != nil {
   524  		return nil, pageInfo, err
   525  	}
   526  
   527  	referees, pageInfo = entities.PageEntities[*v2.TeamRefereeHistoryEdge](referees, pagination)
   528  
   529  	return referees, pageInfo, nil
   530  }