code.vegaprotocol.io/vega@v0.79.0/core/teams/engine.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 teams
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/libs/ptr"
    29  	vegapb "code.vegaprotocol.io/vega/protos/vega"
    30  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    31  	snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1"
    32  
    33  	"golang.org/x/exp/slices"
    34  )
    35  
    36  type Engine struct {
    37  	broker      Broker
    38  	timeService TimeService
    39  
    40  	currentEpoch uint64
    41  
    42  	// minStakedVegaTokens limits referral code generation to parties staking at
    43  	// least this number of tokens.
    44  	minStakedVegaTokens *num.Uint
    45  
    46  	// teams tracks all teams by team ID.
    47  	teams map[types.TeamID]*types.Team
    48  
    49  	// allTeamMembers maps a party to the team they are members of.
    50  	allTeamMembers map[types.PartyID]types.TeamID
    51  
    52  	// teamSwitches tracks all the parties that switch teams. The switch only
    53  	// happens by the end of the epoch.
    54  	teamSwitches map[types.PartyID]teamSwitch
    55  }
    56  
    57  func (e *Engine) OnReferralProgramMinStakedVegaTokensUpdate(_ context.Context, value *num.Uint) error {
    58  	e.minStakedVegaTokens = value
    59  	return nil
    60  }
    61  
    62  func (e *Engine) TeamExists(team types.TeamID) bool {
    63  	_, ok := e.teams[team]
    64  	return ok
    65  }
    66  
    67  func (e *Engine) CreateTeam(ctx context.Context, referrer types.PartyID, deterministicTeamID types.TeamID, params *commandspb.CreateReferralSet_Team) error {
    68  	if err := e.ensureUniqueTeamID(deterministicTeamID); err != nil {
    69  		return err
    70  	}
    71  
    72  	if err := e.ensureUniqueTeamName(params.Name); err != nil {
    73  		return err
    74  	}
    75  
    76  	// are we already a team owner? in which case
    77  	// it's not allowed to create a team
    78  	for _, team := range e.teams {
    79  		if team.Referrer.PartyID == referrer {
    80  			return ErrPartyAlreadyBelongsToTeam(referrer)
    81  		}
    82  	}
    83  
    84  	if len(params.Name) <= 0 {
    85  		return errors.New("missing required team name parameter")
    86  	}
    87  
    88  	// if the party is a member of a team but not a referrer
    89  	// then we need to move it from the previous one, and get
    90  	// and create it.
    91  	prevTeamID, isAlreadyMember := e.allTeamMembers[referrer]
    92  
    93  	// here just removing them from the team would be enough
    94  	// to have the correct step
    95  	//
    96  	// the notify create team event later will in the DN:
    97  	// - create the new team
    98  	// - update the membership informations for the party
    99  	// - all is fine
   100  	if isAlreadyMember {
   101  		e.teams[prevTeamID].RemoveReferee(referrer)
   102  	}
   103  
   104  	now := e.timeService.GetTimeNow()
   105  
   106  	teamToAdd := &types.Team{
   107  		ID: deterministicTeamID,
   108  		Referrer: &types.Membership{
   109  			PartyID:        referrer,
   110  			JoinedAt:       now,
   111  			StartedAtEpoch: e.currentEpoch,
   112  		},
   113  		Name:      params.Name,
   114  		TeamURL:   ptr.UnBox(params.TeamUrl),
   115  		AvatarURL: ptr.UnBox(params.AvatarUrl),
   116  		CreatedAt: now,
   117  		Closed:    params.Closed,
   118  	}
   119  
   120  	if len(params.AllowList) > 0 {
   121  		teamToAdd.AllowList = make([]types.PartyID, 0, len(params.AllowList))
   122  		for _, key := range params.AllowList {
   123  			teamToAdd.AllowList = append(teamToAdd.AllowList, types.PartyID(key))
   124  		}
   125  	}
   126  
   127  	e.teams[deterministicTeamID] = teamToAdd
   128  
   129  	e.allTeamMembers[referrer] = deterministicTeamID
   130  
   131  	e.notifyTeamCreated(ctx, teamToAdd)
   132  
   133  	return nil
   134  }
   135  
   136  func (e *Engine) UpdateTeam(ctx context.Context, referrer types.PartyID, teamID types.TeamID, params *commandspb.UpdateReferralSet_Team) error {
   137  	teamToUpdate, exists := e.teams[teamID]
   138  	if !exists {
   139  		return ErrNoTeamMatchesID(teamID)
   140  	}
   141  
   142  	if teamToUpdate.Referrer.PartyID != referrer {
   143  		return ErrOnlyReferrerCanUpdateTeam
   144  	}
   145  
   146  	// can't update if empty and nil as it's a mandatory field
   147  	if params.Name != nil && len(*params.Name) > 0 {
   148  		teamToUpdate.Name = ptr.UnBox(params.Name)
   149  	}
   150  
   151  	// those apply change if not nil only?
   152  	// to be sure to not erase things by mistake?
   153  	if params.TeamUrl != nil {
   154  		teamToUpdate.TeamURL = ptr.UnBox(params.TeamUrl)
   155  	}
   156  
   157  	if params.AvatarUrl != nil {
   158  		teamToUpdate.AvatarURL = ptr.UnBox(params.AvatarUrl)
   159  	}
   160  
   161  	if params.Closed != nil {
   162  		teamToUpdate.Closed = ptr.UnBox(params.Closed)
   163  	}
   164  
   165  	// prepare the teamSwitches based on the new allowlist
   166  	newAllowList := map[string]struct{}{}
   167  	for _, v := range params.AllowList {
   168  		newAllowList[v] = struct{}{}
   169  	}
   170  
   171  	for _, v := range teamToUpdate.Referees {
   172  		// if the referee is not part of the new allowlist,
   173  		// set it to switch teams at the end of the epoch
   174  		if _, ok := newAllowList[v.PartyID.String()]; !ok {
   175  			e.teamSwitches[v.PartyID] = teamSwitch{
   176  				fromTeam: teamID,
   177  				// keep it empty to notify the intent of removing this
   178  				// party completely from the team.
   179  				toTeam: "",
   180  			}
   181  		}
   182  	}
   183  
   184  	if len(params.AllowList) > 0 {
   185  		teamToUpdate.AllowList = make([]types.PartyID, 0, len(params.AllowList))
   186  		for _, key := range params.AllowList {
   187  			teamToUpdate.AllowList = append(teamToUpdate.AllowList, types.PartyID(key))
   188  		}
   189  	}
   190  
   191  	e.notifyTeamUpdated(ctx, teamToUpdate)
   192  
   193  	return nil
   194  }
   195  
   196  func (e *Engine) JoinTeam(ctx context.Context, referee types.PartyID, params *commandspb.JoinTeam) error {
   197  	for _, team := range e.teams {
   198  		if team.Referrer.PartyID == referee {
   199  			return ErrReferrerCannotJoinAnotherTeam
   200  		}
   201  	}
   202  
   203  	teamID := types.TeamID(params.Id)
   204  
   205  	teamToJoin, exists := e.teams[teamID]
   206  	if !exists {
   207  		return ErrNoTeamMatchesID(teamID)
   208  	}
   209  
   210  	if err := teamToJoin.EnsureCanJoin(referee); err != nil {
   211  		return err
   212  	}
   213  
   214  	teamJoined, alreadyMember := e.allTeamMembers[referee]
   215  	if alreadyMember {
   216  		// This party is already member of a team, it will be moved at the end
   217  		// of epoch.
   218  		e.teamSwitches[referee] = teamSwitch{
   219  			fromTeam: teamJoined,
   220  			toTeam:   teamToJoin.ID,
   221  		}
   222  		return nil
   223  	}
   224  
   225  	// The party does not belong to a team, so he joins right away.
   226  	membership := &types.Membership{
   227  		PartyID:        referee,
   228  		JoinedAt:       e.timeService.GetTimeNow(),
   229  		StartedAtEpoch: e.currentEpoch,
   230  	}
   231  	teamToJoin.Referees = append(teamToJoin.Referees, membership)
   232  
   233  	e.allTeamMembers[referee] = teamToJoin.ID
   234  
   235  	e.notifyRefereeJoinedTeam(ctx, teamToJoin, membership)
   236  
   237  	return nil
   238  }
   239  
   240  func (e *Engine) GetAllPartiesInTeams(minEpochsInTeam uint64) []string {
   241  	parties := make([]string, 0, len(e.allTeamMembers))
   242  
   243  	for t := range e.teams {
   244  		members := e.GetTeamMembers(string(t), minEpochsInTeam)
   245  		if len(members) > 0 {
   246  			parties = append(parties, members...)
   247  		}
   248  	}
   249  	sort.Strings(parties)
   250  	return parties
   251  }
   252  
   253  func (e *Engine) GetAllTeamsWithParties(minEpochsInTeam uint64) map[string][]string {
   254  	teams := make(map[string][]string, len(e.teams))
   255  	for t := range e.teams {
   256  		team := string(t)
   257  		if members := e.GetTeamMembers(team, minEpochsInTeam); len(members) > 0 {
   258  			teams[team] = members
   259  		}
   260  	}
   261  	return teams
   262  }
   263  
   264  func (e *Engine) GetTeamMembers(team string, minEpochsInTeam uint64) []string {
   265  	t := e.teams[(types.TeamID(team))]
   266  	if t == nil {
   267  		return []string{}
   268  	}
   269  	teamMembers := make([]string, 0, len(t.Referees)+1)
   270  	for _, m := range t.Referees {
   271  		if e.currentEpoch-m.StartedAtEpoch >= minEpochsInTeam {
   272  			teamMembers = append(teamMembers, string(m.PartyID))
   273  		}
   274  	}
   275  	if e.currentEpoch-t.Referrer.StartedAtEpoch >= minEpochsInTeam {
   276  		teamMembers = append(teamMembers, string(t.Referrer.PartyID))
   277  	}
   278  	sort.Strings(teamMembers)
   279  	return teamMembers
   280  }
   281  
   282  func (e *Engine) IsTeamMember(party types.PartyID) bool {
   283  	_, isMember := e.allTeamMembers[party]
   284  	return isMember
   285  }
   286  
   287  func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) {
   288  	if ep.Action == vegapb.EpochAction_EPOCH_ACTION_START {
   289  		e.currentEpoch = ep.Seq
   290  		e.moveMembers(ctx, ep.StartTime, ep.Seq)
   291  	}
   292  }
   293  
   294  func (e *Engine) OnEpochRestore(_ context.Context, ep types.Epoch) {
   295  	if ep.Action == vegapb.EpochAction_EPOCH_ACTION_START {
   296  		e.currentEpoch = ep.Seq
   297  	}
   298  }
   299  
   300  // moveMembers ensures members are moved in a deterministic order.
   301  func (e *Engine) moveMembers(ctx context.Context, startEpochTime time.Time, epoch uint64) {
   302  	sortedPartyID := make([]types.PartyID, 0, len(e.teamSwitches))
   303  	for partyID := range e.teamSwitches {
   304  		sortedPartyID = append(sortedPartyID, partyID)
   305  	}
   306  	slices.SortStableFunc(sortedPartyID, func(a, b types.PartyID) int {
   307  		return strings.Compare(string(a), string(b))
   308  	})
   309  
   310  	for _, partyID := range sortedPartyID {
   311  		move := e.teamSwitches[partyID]
   312  		e.teams[move.fromTeam].RemoveReferee(partyID)
   313  		membership := &types.Membership{
   314  			PartyID:        partyID,
   315  			JoinedAt:       startEpochTime,
   316  			StartedAtEpoch: epoch,
   317  		}
   318  
   319  		// if there's no to team, this is a referee which have been
   320  		// remove from an allowlist of a private team.
   321  		if move.toTeam.IsNoTeam() {
   322  			// just fully remove the party from the mapping.
   323  			delete(e.allTeamMembers, partyID)
   324  		} else {
   325  			// do as usual.
   326  			toTeam := e.teams[move.toTeam]
   327  			toTeam.Referees = append(toTeam.Referees, membership)
   328  			e.allTeamMembers[partyID] = toTeam.ID
   329  		}
   330  		e.notifyRefereeSwitchedTeam(ctx, move, membership)
   331  	}
   332  
   333  	e.teamSwitches = map[types.PartyID]teamSwitch{}
   334  }
   335  
   336  func (e *Engine) notifyTeamCreated(ctx context.Context, createdTeam *types.Team) {
   337  	e.broker.Send(events.NewTeamCreatedEvent(ctx, e.currentEpoch, createdTeam))
   338  }
   339  
   340  func (e *Engine) notifyTeamUpdated(ctx context.Context, updatedTeam *types.Team) {
   341  	e.broker.Send(events.NewTeamUpdatedEvent(ctx, updatedTeam))
   342  }
   343  
   344  func (e *Engine) notifyRefereeSwitchedTeam(ctx context.Context, move teamSwitch, membership *types.Membership) {
   345  	e.broker.Send(events.NewRefereeSwitchedTeamEvent(ctx, move.fromTeam, move.toTeam, membership))
   346  }
   347  
   348  func (e *Engine) notifyRefereeJoinedTeam(ctx context.Context, teamID *types.Team, membership *types.Membership) {
   349  	e.broker.Send(events.NewRefereeJoinedTeamEvent(ctx, teamID.ID, membership))
   350  }
   351  
   352  func (e *Engine) ensureUniqueTeamID(deterministicTeamID types.TeamID) error {
   353  	if _, exists := e.teams[deterministicTeamID]; exists {
   354  		return ErrComputedTeamIDIsAlreadyInUse
   355  	}
   356  	return nil
   357  }
   358  
   359  func (e *Engine) ensureUniqueTeamName(name string) error {
   360  	for _, team := range e.teams {
   361  		if team.Name == name {
   362  			return ErrTeamNameIsAlreadyInUse
   363  		}
   364  	}
   365  
   366  	return nil
   367  }
   368  
   369  func (e *Engine) loadTeamsFromSnapshot(teamsSnapshot []*snapshotpb.Team) {
   370  	for _, teamSnapshot := range teamsSnapshot {
   371  		teamID := types.TeamID(teamSnapshot.Id)
   372  
   373  		referrerID := types.PartyID(teamSnapshot.Referrer.PartyId)
   374  		e.allTeamMembers[referrerID] = teamID
   375  
   376  		referees := make([]*types.Membership, 0, len(teamSnapshot.Referees))
   377  		for _, refereeSnapshot := range teamSnapshot.Referees {
   378  			refereeID := types.PartyID(refereeSnapshot.PartyId)
   379  			e.allTeamMembers[refereeID] = teamID
   380  			referees = append(referees, &types.Membership{
   381  				PartyID:        refereeID,
   382  				JoinedAt:       time.Unix(0, refereeSnapshot.JoinedAt),
   383  				StartedAtEpoch: refereeSnapshot.StartedAtEpoch,
   384  			})
   385  		}
   386  
   387  		t := &types.Team{
   388  			ID: teamID,
   389  			Referrer: &types.Membership{
   390  				PartyID:        referrerID,
   391  				JoinedAt:       time.Unix(0, teamSnapshot.Referrer.JoinedAt),
   392  				StartedAtEpoch: teamSnapshot.Referrer.StartedAtEpoch,
   393  			},
   394  			Referees:  referees,
   395  			Name:      teamSnapshot.Name,
   396  			TeamURL:   teamSnapshot.TeamUrl,
   397  			AvatarURL: teamSnapshot.AvatarUrl,
   398  			CreatedAt: time.Unix(0, teamSnapshot.CreatedAt),
   399  			Closed:    teamSnapshot.Closed,
   400  		}
   401  
   402  		if len(teamSnapshot.AllowList) > 0 {
   403  			t.AllowList = make([]types.PartyID, 0, len(teamSnapshot.AllowList))
   404  			for _, partyIDStr := range teamSnapshot.AllowList {
   405  				t.AllowList = append(t.AllowList, types.PartyID(partyIDStr))
   406  			}
   407  		}
   408  
   409  		e.teams[teamID] = t
   410  	}
   411  }
   412  
   413  func (e *Engine) loadTeamSwitchesFromSnapshot(teamSwitchesSnapshot []*snapshotpb.TeamSwitch) {
   414  	for _, teamSwitchSnapshot := range teamSwitchesSnapshot {
   415  		partyID := types.PartyID(teamSwitchSnapshot.PartyId)
   416  		e.teamSwitches[partyID] = teamSwitch{
   417  			fromTeam: types.TeamID(teamSwitchSnapshot.FromTeamId),
   418  			toTeam:   types.TeamID(teamSwitchSnapshot.ToTeamId),
   419  		}
   420  	}
   421  }
   422  
   423  func NewEngine(broker Broker, timeSvc TimeService) *Engine {
   424  	engine := &Engine{
   425  		broker:      broker,
   426  		timeService: timeSvc,
   427  
   428  		teams:          map[types.TeamID]*types.Team{},
   429  		allTeamMembers: map[types.PartyID]types.TeamID{},
   430  		teamSwitches:   map[types.PartyID]teamSwitch{},
   431  
   432  		minStakedVegaTokens: num.UintZero(),
   433  	}
   434  
   435  	return engine
   436  }
   437  
   438  type teamSwitch struct {
   439  	fromTeam types.TeamID
   440  	toTeam   types.TeamID
   441  }