code.vegaprotocol.io/vega@v0.79.0/core/activitystreak/activitiystreak_test.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 activitystreak_test 17 18 import ( 19 "context" 20 "testing" 21 22 "code.vegaprotocol.io/vega/core/activitystreak" 23 "code.vegaprotocol.io/vega/core/activitystreak/mocks" 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/types" 26 "code.vegaprotocol.io/vega/libs/num" 27 "code.vegaprotocol.io/vega/logging" 28 vegapb "code.vegaprotocol.io/vega/protos/vega" 29 30 "github.com/golang/mock/gomock" 31 "github.com/stretchr/testify/assert" 32 ) 33 34 type testEngine struct { 35 *activitystreak.Engine 36 37 ctrl *gomock.Controller 38 broker *mocks.MockBroker 39 marketsStats *mocks.MockMarketsStatsAggregator 40 } 41 42 func getTestEngine(t *testing.T) *testEngine { 43 t.Helper() 44 ctrl := gomock.NewController(t) 45 marketsStats := mocks.NewMockMarketsStatsAggregator(ctrl) 46 broker := mocks.NewMockBroker(ctrl) 47 48 return &testEngine{ 49 Engine: activitystreak.New( 50 logging.NewTestLogger(), marketsStats, broker, 51 ), 52 ctrl: ctrl, 53 broker: broker, 54 marketsStats: marketsStats, 55 } 56 } 57 58 func TestStreak(t *testing.T) { 59 engine := getTestEngine(t) 60 61 engine.OnMinQuantumOpenNationalVolumeUpdate(context.Background(), num.NewUint(100)) 62 engine.OnMinQuantumTradeVolumeUpdate(context.Background(), num.NewUint(200)) 63 engine.OnRewardsActivityStreakInactivityLimit(context.Background(), num.NewUint(10)) 64 assert.NoError(t, engine.OnBenefitTiersUpdate(context.Background(), &vegapb.ActivityStreakBenefitTiers{ 65 Tiers: []*vegapb.ActivityStreakBenefitTier{ 66 { 67 MinimumActivityStreak: 1, 68 RewardMultiplier: "2", 69 VestingMultiplier: "1.5", 70 }, 71 { 72 MinimumActivityStreak: 7, 73 RewardMultiplier: "3", 74 VestingMultiplier: "2.5", 75 }, 76 { 77 MinimumActivityStreak: 14, 78 RewardMultiplier: "4", 79 VestingMultiplier: "3.5", 80 }, 81 }, 82 })) 83 84 t.Run("no streak for a party == 1x", func(t *testing.T) { 85 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 86 87 assert.Equal(t, num.DecimalOne(), tradeX) 88 assert.Equal(t, num.DecimalOne(), volumeX) 89 }) 90 91 t.Run("add volume < min == 1x", func(t *testing.T) { 92 engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return( 93 map[string]*types.MarketStats{ 94 "market1": { 95 PartiesOpenNotionalVolume: map[string]*num.Uint{ 96 "party1": num.NewUint(20), 97 }, 98 PartiesTotalTradeVolume: map[string]*num.Uint{ 99 "party1": num.NewUint(50), 100 }, 101 }, 102 "market2": { 103 PartiesOpenNotionalVolume: map[string]*num.Uint{ 104 "party1": num.NewUint(20), 105 }, 106 PartiesTotalTradeVolume: map[string]*num.Uint{ 107 "party1": num.NewUint(50), 108 }, 109 }, 110 }, 111 ) 112 113 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 114 func(evts []events.Event) { 115 assert.Len(t, evts, 1) 116 117 pas := evts[0].(*events.PartyActivityStreak) 118 assert.False(t, pas.Proto().IsActive) 119 assert.Equal(t, int(pas.Proto().ActiveFor), 0) 120 assert.Equal(t, int(pas.Proto().InactiveFor), 1) 121 assert.Equal(t, int(pas.Proto().Epoch), 1) 122 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "1") 123 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1") 124 }, 125 ) 126 127 engine.OnEpochEvent(context.Background(), types.Epoch{ 128 Seq: 1, 129 Action: vegapb.EpochAction_EPOCH_ACTION_END, 130 }) 131 132 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 133 134 assert.Equal(t, num.DecimalOne(), tradeX) 135 assert.Equal(t, num.DecimalOne(), volumeX) 136 }) 137 138 t.Run("add volume > min == increase multipliers", func(t *testing.T) { 139 engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return( 140 map[string]*types.MarketStats{ 141 "market1": { 142 PartiesOpenNotionalVolume: map[string]*num.Uint{ 143 "party1": num.NewUint(100), 144 }, 145 PartiesTotalTradeVolume: map[string]*num.Uint{ 146 "party1": num.NewUint(50), 147 }, 148 }, 149 "market2": { 150 PartiesOpenNotionalVolume: map[string]*num.Uint{ 151 "party1": num.NewUint(20), 152 }, 153 PartiesTotalTradeVolume: map[string]*num.Uint{ 154 "party1": num.NewUint(50), 155 }, 156 }, 157 }, 158 ) 159 160 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 161 func(evts []events.Event) { 162 assert.Len(t, evts, 1) 163 164 pas := evts[0].(*events.PartyActivityStreak) 165 assert.True(t, pas.Proto().IsActive) 166 assert.Equal(t, int(pas.Proto().ActiveFor), 1) 167 assert.Equal(t, int(pas.Proto().InactiveFor), 0) 168 assert.Equal(t, int(pas.Proto().Epoch), 2) 169 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "2") 170 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1.5") 171 }, 172 ) 173 174 engine.OnEpochEvent(context.Background(), types.Epoch{ 175 Seq: 2, 176 Action: vegapb.EpochAction_EPOCH_ACTION_END, 177 }) 178 179 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 180 181 assert.Equal(t, num.MustDecimalFromString("2"), tradeX) 182 assert.Equal(t, num.MustDecimalFromString("1.5"), volumeX) 183 }) 184 185 t.Run("add volume > min many time == move to next tier", func(t *testing.T) { 186 engine.marketsStats.EXPECT().GetMarketStats().Times(6).Return( 187 map[string]*types.MarketStats{ 188 "market1": { 189 PartiesOpenNotionalVolume: map[string]*num.Uint{ 190 "party1": num.NewUint(100), 191 }, 192 PartiesTotalTradeVolume: map[string]*num.Uint{ 193 "party1": num.NewUint(50), 194 }, 195 }, 196 "market2": { 197 PartiesOpenNotionalVolume: map[string]*num.Uint{ 198 "party1": num.NewUint(20), 199 }, 200 PartiesTotalTradeVolume: map[string]*num.Uint{ 201 "party1": num.NewUint(50), 202 }, 203 }, 204 }, 205 ) 206 207 // discard first 5 208 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(5) 209 210 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 211 func(evts []events.Event) { 212 assert.Len(t, evts, 1) 213 214 pas := evts[0].(*events.PartyActivityStreak) 215 assert.True(t, pas.Proto().IsActive) 216 assert.Equal(t, int(pas.Proto().ActiveFor), 7) 217 assert.Equal(t, int(pas.Proto().InactiveFor), 0) 218 assert.Equal(t, int(pas.Proto().Epoch), 8) 219 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3") 220 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5") 221 }, 222 ) 223 224 for i := 3; i <= 8; i++ { 225 engine.OnEpochEvent(context.Background(), types.Epoch{ 226 Seq: uint64(i), 227 Action: vegapb.EpochAction_EPOCH_ACTION_END, 228 }) 229 } 230 231 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 232 233 assert.Equal(t, num.MustDecimalFromString("3"), tradeX) 234 assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX) 235 }) 236 237 t.Run("add volume < min less times than current streak == inactive but still have benefits", func(t *testing.T) { 238 engine.marketsStats.EXPECT().GetMarketStats().Times(4).Return( 239 map[string]*types.MarketStats{ 240 "market1": { 241 PartiesOpenNotionalVolume: map[string]*num.Uint{ 242 "party1": num.NewUint(20), 243 }, 244 PartiesTotalTradeVolume: map[string]*num.Uint{ 245 "party1": num.NewUint(50), 246 }, 247 }, 248 "market2": { 249 PartiesOpenNotionalVolume: map[string]*num.Uint{ 250 "party1": num.NewUint(20), 251 }, 252 PartiesTotalTradeVolume: map[string]*num.Uint{ 253 "party1": num.NewUint(50), 254 }, 255 }, 256 }, 257 ) 258 259 // discard first 5 260 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(3) 261 262 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 263 func(evts []events.Event) { 264 assert.Len(t, evts, 1) 265 266 pas := evts[0].(*events.PartyActivityStreak) 267 assert.False(t, pas.Proto().IsActive) 268 assert.Equal(t, int(pas.Proto().ActiveFor), 7) 269 assert.Equal(t, int(pas.Proto().InactiveFor), 4) 270 assert.Equal(t, int(pas.Proto().Epoch), 12) 271 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3") 272 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5") 273 }, 274 ) 275 276 for i := 9; i <= 12; i++ { 277 engine.OnEpochEvent(context.Background(), types.Epoch{ 278 Seq: uint64(i), 279 Action: vegapb.EpochAction_EPOCH_ACTION_END, 280 }) 281 } 282 283 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 284 285 assert.Equal(t, num.MustDecimalFromString("3"), tradeX) 286 assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX) 287 }) 288 289 t.Run("add volume > min again == becomes active again", func(t *testing.T) { 290 engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return( 291 map[string]*types.MarketStats{ 292 "market1": { 293 PartiesOpenNotionalVolume: map[string]*num.Uint{ 294 "party1": num.NewUint(100), 295 }, 296 PartiesTotalTradeVolume: map[string]*num.Uint{ 297 "party1": num.NewUint(50), 298 }, 299 }, 300 "market2": { 301 PartiesOpenNotionalVolume: map[string]*num.Uint{ 302 "party1": num.NewUint(20), 303 }, 304 PartiesTotalTradeVolume: map[string]*num.Uint{ 305 "party1": num.NewUint(50), 306 }, 307 }, 308 }, 309 ) 310 311 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 312 func(evts []events.Event) { 313 assert.Len(t, evts, 1) 314 315 pas := evts[0].(*events.PartyActivityStreak) 316 assert.True(t, pas.Proto().IsActive) 317 assert.Equal(t, int(pas.Proto().ActiveFor), 8) 318 assert.Equal(t, int(pas.Proto().InactiveFor), 0) 319 assert.Equal(t, int(pas.Proto().Epoch), 13) 320 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3") 321 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5") 322 }, 323 ) 324 325 engine.OnEpochEvent(context.Background(), types.Epoch{ 326 Seq: uint64(13), 327 Action: vegapb.EpochAction_EPOCH_ACTION_END, 328 }) 329 330 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 331 332 assert.Equal(t, num.MustDecimalFromString("3"), tradeX) 333 assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX) 334 }) 335 336 t.Run("add volume < min more times than current streak looses benefits", func(t *testing.T) { 337 engine.marketsStats.EXPECT().GetMarketStats().Times(11).Return( 338 map[string]*types.MarketStats{ 339 "market1": { 340 PartiesOpenNotionalVolume: map[string]*num.Uint{ 341 "party1": num.NewUint(20), 342 }, 343 PartiesTotalTradeVolume: map[string]*num.Uint{ 344 "party1": num.NewUint(50), 345 }, 346 }, 347 "market2": { 348 PartiesOpenNotionalVolume: map[string]*num.Uint{ 349 "party1": num.NewUint(20), 350 }, 351 PartiesTotalTradeVolume: map[string]*num.Uint{ 352 "party1": num.NewUint(50), 353 }, 354 }, 355 }, 356 ) 357 358 // discard first 5 359 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(10) 360 361 engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do( 362 func(evts []events.Event) { 363 assert.Len(t, evts, 1) 364 365 pas := evts[0].(*events.PartyActivityStreak) 366 assert.False(t, pas.Proto().IsActive) 367 assert.Equal(t, int(pas.Proto().ActiveFor), 0) 368 assert.Equal(t, int(pas.Proto().InactiveFor), 11) 369 assert.Equal(t, int(pas.Proto().Epoch), 24) 370 assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "1") 371 assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1") 372 }, 373 ) 374 375 for i := 14; i <= 24; i++ { 376 engine.OnEpochEvent(context.Background(), types.Epoch{ 377 Seq: uint64(i), 378 Action: vegapb.EpochAction_EPOCH_ACTION_END, 379 }) 380 } 381 382 tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1") 383 384 assert.Equal(t, num.MustDecimalFromString("1"), tradeX) 385 assert.Equal(t, num.MustDecimalFromString("1"), volumeX) 386 }) 387 }