diff --git a/CHANGELOG.md b/CHANGELOG.md index 984509e898..198c5540ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - [11644](https://github.com/vegaprotocol/vega/issues/11644) - `liveOnly` flag has been added to the `AMM` API to show only active `AMMs`. - [11519](https://github.com/vegaprotocol/vega/issues/11519) - Add fees to position API types. - [11642](https://github.com/vegaprotocol/vega/issues/11642) - `AMMs` with empty price levels are now allowed. +- [11711](https://github.com/vegaprotocol/vega/issues/11711) - Manage closed team membership by updating the allow list. ### 🐛 Fixes diff --git a/core/teams/engine.go b/core/teams/engine.go index 3ef7cc9f07..22521a4124 100644 --- a/core/teams/engine.go +++ b/core/teams/engine.go @@ -134,42 +134,61 @@ func (e *Engine) CreateTeam(ctx context.Context, referrer types.PartyID, determi } func (e *Engine) UpdateTeam(ctx context.Context, referrer types.PartyID, teamID types.TeamID, params *commandspb.UpdateReferralSet_Team) error { - teamsToUpdate, exists := e.teams[teamID] + teamToUpdate, exists := e.teams[teamID] if !exists { return ErrNoTeamMatchesID(teamID) } - if teamsToUpdate.Referrer.PartyID != referrer { + if teamToUpdate.Referrer.PartyID != referrer { return ErrOnlyReferrerCanUpdateTeam } // can't update if empty and nil as it's a mandatory field if params.Name != nil && len(*params.Name) > 0 { - teamsToUpdate.Name = ptr.UnBox(params.Name) + teamToUpdate.Name = ptr.UnBox(params.Name) } // those apply change if not nil only? // to be sure to not erase things by mistake? if params.TeamUrl != nil { - teamsToUpdate.TeamURL = ptr.UnBox(params.TeamUrl) + teamToUpdate.TeamURL = ptr.UnBox(params.TeamUrl) } if params.AvatarUrl != nil { - teamsToUpdate.AvatarURL = ptr.UnBox(params.AvatarUrl) + teamToUpdate.AvatarURL = ptr.UnBox(params.AvatarUrl) } if params.Closed != nil { - teamsToUpdate.Closed = ptr.UnBox(params.Closed) + teamToUpdate.Closed = ptr.UnBox(params.Closed) + } + + // prepare the teamSwitches based on the new allowlist + newAllowList := map[string]struct{}{} + for _, v := range params.AllowList { + newAllowList[v] = struct{}{} + } + + for _, v := range teamToUpdate.Referees { + // if the referee is not part of the new allowlist, + // set it to switch teams at the end of the epoch + if _, ok := newAllowList[v.PartyID.String()]; !ok { + e.teamSwitches[v.PartyID] = teamSwitch{ + fromTeam: teamID, + // keep it empty to notify the intent of removing this + // party completely from the team. + toTeam: "", + } + } } if len(params.AllowList) > 0 { - teamsToUpdate.AllowList = make([]types.PartyID, 0, len(params.AllowList)) + teamToUpdate.AllowList = make([]types.PartyID, 0, len(params.AllowList)) for _, key := range params.AllowList { - teamsToUpdate.AllowList = append(teamsToUpdate.AllowList, types.PartyID(key)) + teamToUpdate.AllowList = append(teamToUpdate.AllowList, types.PartyID(key)) } } - e.notifyTeamUpdated(ctx, teamsToUpdate) + e.notifyTeamUpdated(ctx, teamToUpdate) return nil } @@ -296,10 +315,18 @@ func (e *Engine) moveMembers(ctx context.Context, startEpochTime time.Time, epoc JoinedAt: startEpochTime, StartedAtEpoch: epoch, } - toTeam := e.teams[move.toTeam] - toTeam.Referees = append(toTeam.Referees, membership) - e.allTeamMembers[partyID] = toTeam.ID + // if there's no to team, this is a referee which have been + // remove from an allowlist of a private team. + if move.toTeam.IsNoTeam() { + // just fully remove the party from the mapping. + delete(e.allTeamMembers, partyID) + } else { + // do as usual. + toTeam := e.teams[move.toTeam] + toTeam.Referees = append(toTeam.Referees, membership) + e.allTeamMembers[partyID] = toTeam.ID + } e.notifyRefereeSwitchedTeam(ctx, move, membership) } diff --git a/core/teams/engine_test.go b/core/teams/engine_test.go index 675c766afa..7923a8e76a 100644 --- a/core/teams/engine_test.go +++ b/core/teams/engine_test.go @@ -708,3 +708,54 @@ func testMinEpochsRequired(t *testing.T) { members = te.engine.GetTeamMembers(string(teamID1), 5) assert.Len(t, members, 2) } + +func TestRemoveFromAllowListRemoveFromTheTeam(t *testing.T) { + ctx := vgtest.VegaContext(vgrand.RandomStr(5), vgtest.RandomI64()) + + te := newEngine(t) + + require.False(t, te.engine.TeamExists(newTeamID(t))) + + referrer1 := newPartyID(t) + referee1 := newPartyID(t) + teamID1 := newTeamID(t) + name := vgrand.RandomStr(5) + teamURL := "https://" + name + ".io" + avatarURL := "https://avatar." + name + ".io" + + // create the team + expectTeamCreatedEvent(t, te) + team1CreationDate := time.Now() + te.timeService.EXPECT().GetTimeNow().Return(team1CreationDate).Times(1) + + te.engine.OnEpoch(ctx, types.Epoch{Seq: 1, Action: vegapb.EpochAction_EPOCH_ACTION_START}) + + require.NoError(t, te.engine.CreateTeam(ctx, referrer1, teamID1, + createTeamWithAllowListCmd(t, name, teamURL, avatarURL, true, []string{referee1.String()}))) + require.True(t, te.engine.TeamExists(teamID1)) + + // referee join the team + expectRefereeJoinedTeamEvent(t, te) + te.timeService.EXPECT().GetTimeNow().Return(team1CreationDate.Add(10 * time.Second)).Times(1) + + require.NoError(t, te.engine.JoinTeam(ctx, referee1, joinTeamCmd(t, teamID1))) + require.True(t, te.engine.IsTeamMember(referee1)) + // two members in the team + assert.Len(t, te.engine.GetTeamMembers(string(teamID1), 0), 2) + + te.engine.OnEpoch(ctx, types.Epoch{Seq: 2, Action: vegapb.EpochAction_EPOCH_ACTION_START}) + + // referrer update the team to remove all allowlisted parties + expectTeamUpdatedEvent(t, te) + // te.timeService.EXPECT().GetTimeNow().Return(team1CreationDate.Add(20 * time.Second)).Times(1) + require.NoError(t, te.engine.UpdateTeam(ctx, referrer1, teamID1, + updateTeamCmd(t, name, teamURL, avatarURL, true, []string{}))) + require.True(t, te.engine.TeamExists(teamID1)) + + // move to the next epoch + expectRefereeSwitchedTeamEvent(t, te) + te.engine.OnEpoch(ctx, types.Epoch{Seq: 2, Action: vegapb.EpochAction_EPOCH_ACTION_START}) + require.False(t, te.engine.IsTeamMember(referee1)) + // only referrer is team member now + assert.Len(t, te.engine.GetTeamMembers(string(teamID1), 0), 1) +} diff --git a/core/teams/helpers_for_test.go b/core/teams/helpers_for_test.go index b148fab0cb..98bc37eccd 100644 --- a/core/teams/helpers_for_test.go +++ b/core/teams/helpers_for_test.go @@ -241,6 +241,18 @@ func createTeamCmd(t *testing.T, name, teamURL, avatarURL string) *commandspb.Cr } } +func createTeamWithAllowListCmd(t *testing.T, name, teamURL, avatarURL string, closed bool, allowList []string) *commandspb.CreateReferralSet_Team { + t.Helper() + + return &commandspb.CreateReferralSet_Team{ + Name: name, + TeamUrl: ptr.From(teamURL), + AvatarUrl: ptr.From(avatarURL), + Closed: closed, + AllowList: allowList, + } +} + func updateTeamCmd(t *testing.T, name, teamURL, avatarURL string, closed bool, allowList []string) *commandspb.UpdateReferralSet_Team { t.Helper() diff --git a/core/types/team.go b/core/types/team.go index 2af5dadfde..1521048030 100644 --- a/core/types/team.go +++ b/core/types/team.go @@ -26,6 +26,10 @@ const teamIDLength = 64 type TeamID string +func (t TeamID) IsNoTeam() bool { + return len(string(t)) <= 0 +} + type Team struct { ID TeamID diff --git a/datanode/sqlstore/teams.go b/datanode/sqlstore/teams.go index 00c78e314e..c408580b70 100644 --- a/datanode/sqlstore/teams.go +++ b/datanode/sqlstore/teams.go @@ -256,6 +256,18 @@ func (t *Teams) RefereeJoinedTeam(ctx context.Context, referee *entities.TeamMem func (t *Teams) RefereeSwitchedTeam(ctx context.Context, referee *entities.RefereeTeamSwitch) error { defer metrics.StartSQLQuery("Teams", "RefereeSwitchedTeam")() + // in case the party was removed from the team owner from a team + if len(referee.ToTeamID) <= 0 { + _, err := t.Exec( + ctx, + "DELETE FROM team_members WHERE party_id = $1", + referee.PartyID, + ) + + return err + } + + // normal path, team_members just being updated. _, err := t.Exec(ctx, `INSERT INTO team_members(team_id, party_id, joined_at, joined_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5)`, referee.ToTeamID,