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 }