code.vegaprotocol.io/vega@v0.79.0/datanode/sqlsubscribers/funding_payments_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 sqlsubscribers_test 17 18 import ( 19 "context" 20 "testing" 21 "time" 22 23 "code.vegaprotocol.io/vega/core/events" 24 "code.vegaprotocol.io/vega/core/types" 25 "code.vegaprotocol.io/vega/datanode/entities" 26 "code.vegaprotocol.io/vega/datanode/sqlsubscribers" 27 "code.vegaprotocol.io/vega/datanode/sqlsubscribers/mocks" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/libs/ptr" 30 31 "github.com/golang/mock/gomock" 32 "github.com/stretchr/testify/require" 33 ) 34 35 type fpSub struct { 36 *sqlsubscribers.FundingPaymentSubscriber 37 ctrl *gomock.Controller 38 store *mocks.MockFundingPaymentsStore 39 } 40 41 func TestBasicInterface(t *testing.T) { 42 sub := getFundingPaymentsSub(t) 43 ctx := context.Background() 44 defer sub.ctrl.Finish() 45 types := sub.Types() 46 require.Equal(t, 2, len(types)) 47 require.EqualValues(t, []events.Type{ 48 events.FundingPaymentsEvent, 49 events.LossSocializationEvent, 50 }, types) 51 require.NoError(t, sub.Flush(ctx)) 52 } 53 54 func TestUnmatchedLossSocCases(t *testing.T) { 55 t.Run("simple case getting the previous record and adding a new one with amount lost", testUnmatchedLossSoc) 56 t.Run("excess from loss socialisation sets amount lost to zero", testUnmatchedZeroOutLoss) 57 t.Run("excess from loss socialisation increases amount", testUnmatchedIncreasesAmount) 58 t.Run("excess from loss socialisation decreases loss amount", testUnmatchedDecreasesLoss) 59 } 60 61 func TestCachedFundingPaymentsAndLossSocialisation(t *testing.T) { 62 t.Run("expected flow: funding payment followed by loss socialisation events", processFundingPaymentThenLossSocialisation) 63 } 64 65 func processFundingPaymentThenLossSocialisation(t *testing.T) { 66 sub := getFundingPaymentsSub(t) 67 defer sub.ctrl.Finish() 68 ctx := context.Background() 69 now := time.Now() 70 party, market := "partyID", "marketID" 71 won := uint64(1000) 72 seq := uint64(123) 73 loss := num.NewUint(100) 74 // first, send the funding payment events: party wins 1000 75 fEvt := events.NewFundingPaymentsEvent(ctx, market, seq, []events.Transfer{ 76 getTransferEvent(party, market, num.NewUint(won), false), 77 }) 78 var got entities.FundingPayment 79 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).Do(func(_ context.Context, data []*entities.FundingPayment) { 80 profit := num.DecimalFromFloat(float64(won)) 81 require.Equal(t, 1, len(data)) 82 require.True(t, profit.Equal(data[0].Amount)) 83 require.True(t, data[0].LossSocialisationAmount.IsZero()) 84 require.Equal(t, seq, data[0].FundingPeriodSeq) 85 got = *data[0] 86 }).Return(nil) 87 88 // step 1: send the funding payment events, see if we get the data we expect. 89 sub.Push(ctx, fEvt) 90 91 // now create the loss socialisation event. 92 evt := events.NewLossSocializationEvent(ctx, party, market, loss, true, now.Unix(), types.LossTypeFunding) 93 // make sure we process this event 94 require.True(t, evt.IsFunding()) 95 96 // should be in cache, so no need to expect a call here 97 // sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(0) 98 // use the DoAndReturn to make sure the entitiy is updated correctly 99 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error { 100 require.Equal(t, 1, len(data)) 101 require.Equal(t, got.PartyID, data[0].PartyID) 102 require.Equal(t, got.MarketID, data[0].MarketID) 103 require.Equal(t, got.FundingPeriodSeq, data[0].FundingPeriodSeq) 104 require.True(t, data[0].LossSocialisationAmount.IsNegative()) 105 require.True(t, data[0].LossSocialisationAmount.IsInteger()) 106 require.Equal(t, loss.String(), data[0].LossSocialisationAmount.Abs().String()) 107 // amounts are untouched 108 require.True(t, got.Amount.Equal(data[0].Amount)) 109 return nil 110 }) 111 112 sub.Push(ctx, evt) 113 // make sure to reset the cache 114 sub.Flush(ctx) 115 } 116 117 func testUnmatchedLossSoc(t *testing.T) { 118 sub := getFundingPaymentsSub(t) 119 defer sub.ctrl.Finish() 120 ctx := context.Background() 121 last := time.Now().Add(-1 * time.Second) 122 party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix() 123 loss := num.NewUint(100) 124 get := entities.FundingPayment{ 125 PartyID: entities.PartyID(party), 126 MarketID: entities.MarketID(market), 127 FundingPeriodSeq: 123, 128 VegaTime: last, 129 Amount: num.DecimalZero(), 130 LossSocialisationAmount: num.DecimalZero(), 131 } 132 evt := events.NewLossSocializationEvent(ctx, party, market, loss, true, nowTS, types.LossTypeFunding) 133 // make sure we process this event 134 require.True(t, evt.IsFunding()) 135 136 sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil) 137 // use the DoAndReturn to make sure the entitiy is updated correctly 138 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error { 139 require.Equal(t, 1, len(data)) 140 require.Equal(t, get.PartyID, data[0].PartyID) 141 require.Equal(t, get.MarketID, data[0].MarketID) 142 require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq) 143 require.True(t, data[0].LossSocialisationAmount.IsNegative()) 144 require.True(t, data[0].LossSocialisationAmount.IsInteger()) 145 require.Equal(t, loss.String(), data[0].LossSocialisationAmount.Abs().String()) 146 return nil 147 }) 148 149 sub.Push(ctx, evt) 150 // make sure to reset the cache 151 sub.Flush(ctx) 152 } 153 154 func testUnmatchedZeroOutLoss(t *testing.T) { 155 sub := getFundingPaymentsSub(t) 156 defer sub.ctrl.Finish() 157 ctx := context.Background() 158 last := time.Now().Add(-1 * time.Second) 159 party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix() 160 loss := num.NewUint(100) 161 get := entities.FundingPayment{ 162 PartyID: entities.PartyID(party), 163 MarketID: entities.MarketID(market), 164 FundingPeriodSeq: 123, 165 VegaTime: last, 166 Amount: num.DecimalZero(), 167 LossSocialisationAmount: num.DecimalFromUint(loss).Neg(), 168 } 169 evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding) 170 // make sure we process this event 171 require.True(t, evt.IsFunding()) 172 173 sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil) 174 // use the DoAndReturn to make sure the entitiy is updated correctly 175 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error { 176 require.Equal(t, 1, len(data)) 177 require.Equal(t, get.PartyID, data[0].PartyID) 178 require.Equal(t, get.MarketID, data[0].MarketID) 179 require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq) 180 require.True(t, data[0].LossSocialisationAmount.IsZero()) 181 return nil 182 }) 183 184 sub.Push(ctx, evt) 185 // make sure to reset the cache 186 sub.Flush(ctx) 187 } 188 189 func testUnmatchedIncreasesAmount(t *testing.T) { 190 sub := getFundingPaymentsSub(t) 191 defer sub.ctrl.Finish() 192 ctx := context.Background() 193 last := time.Now().Add(-1 * time.Second) 194 party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix() 195 loss := num.NewUint(11) 196 get := entities.FundingPayment{ 197 PartyID: entities.PartyID(party), 198 MarketID: entities.MarketID(market), 199 FundingPeriodSeq: 123, 200 VegaTime: last, 201 Amount: num.DecimalFromFloat(1000), 202 LossSocialisationAmount: num.DecimalFromFloat(1).Neg(), 203 } 204 evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding) 205 // make sure we process this event 206 require.True(t, evt.IsFunding()) 207 208 sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil) 209 // use the DoAndReturn to make sure the entitiy is updated correctly 210 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error { 211 excess := num.DecimalFromUint(loss).Add(get.LossSocialisationAmount) 212 require.Equal(t, 1, len(data)) 213 require.Equal(t, get.PartyID, data[0].PartyID) 214 require.Equal(t, get.MarketID, data[0].MarketID) 215 require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq) 216 require.True(t, data[0].LossSocialisationAmount.IsZero()) // loss is zeroed out 217 // the new amount equals the excess in additional payout (so amount + extra pay - loss amount) 218 require.True(t, get.Amount.Add(excess).Equal(data[0].Amount)) 219 return nil 220 }) 221 222 sub.Push(ctx, evt) 223 // make sure to reset the cache 224 sub.Flush(ctx) 225 } 226 227 func testUnmatchedDecreasesLoss(t *testing.T) { 228 sub := getFundingPaymentsSub(t) 229 defer sub.ctrl.Finish() 230 ctx := context.Background() 231 last := time.Now().Add(-1 * time.Second) 232 party, market, nowTS := "partyID", "marketID", last.Add(time.Second).Unix() 233 loss := num.NewUint(10) 234 get := entities.FundingPayment{ 235 PartyID: entities.PartyID(party), 236 MarketID: entities.MarketID(market), 237 FundingPeriodSeq: 123, 238 VegaTime: last, 239 Amount: num.DecimalFromFloat(1000), 240 LossSocialisationAmount: num.DecimalFromFloat(100).Neg(), 241 } 242 evt := events.NewLossSocializationEvent(ctx, party, market, loss, false, nowTS, types.LossTypeFunding) 243 // make sure we process this event 244 require.True(t, evt.IsFunding()) 245 246 sub.store.EXPECT().GetByPartyAndMarket(gomock.Any(), party, market).Times(1).Return(get, nil) 247 // use the DoAndReturn to make sure the entitiy is updated correctly 248 sub.store.EXPECT().Add(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, data []*entities.FundingPayment) error { 249 require.Equal(t, 1, len(data)) 250 require.Equal(t, get.PartyID, data[0].PartyID) 251 require.Equal(t, get.MarketID, data[0].MarketID) 252 require.Equal(t, get.FundingPeriodSeq, data[0].FundingPeriodSeq) 253 // loss soc has been reduced by the amount from the event 254 require.True(t, data[0].LossSocialisationAmount.Equal(get.LossSocialisationAmount.Add(num.DecimalFromUint(loss)))) // loss is zeroed out 255 // amount paid hasn't changed 256 require.True(t, get.Amount.Equal(data[0].Amount)) 257 return nil 258 }) 259 260 sub.Push(ctx, evt) 261 // make sure to reset the cache 262 sub.Flush(ctx) 263 } 264 265 func getFundingPaymentsSub(t *testing.T) *fpSub { 266 t.Helper() 267 ctrl := gomock.NewController(t) 268 store := mocks.NewMockFundingPaymentsStore(ctrl) 269 sub := sqlsubscribers.NewFundingPaymentsSubscriber(store) 270 return &fpSub{ 271 FundingPaymentSubscriber: sub, 272 ctrl: ctrl, 273 store: store, 274 } 275 } 276 277 type mpStub struct { 278 party string 279 market string 280 amount *num.Uint 281 loss bool 282 } 283 284 func (m *mpStub) Party() string { 285 return m.party 286 } 287 288 func (m *mpStub) Size() int64 { 289 return 0 290 } 291 292 func (m *mpStub) Buy() int64 { 293 return 0 294 } 295 296 func (m *mpStub) Sell() int64 { 297 return 0 298 } 299 300 func (m *mpStub) Price() *num.Uint { 301 return num.UintZero() 302 } 303 304 func (m *mpStub) BuySumProduct() *num.Uint { 305 return num.UintZero() 306 } 307 308 func (m *mpStub) SellSumProduct() *num.Uint { 309 return num.UintZero() 310 } 311 312 func (m *mpStub) VWBuy() *num.Uint { 313 return num.UintZero() 314 } 315 316 func (m *mpStub) VWSell() *num.Uint { 317 return num.UintZero() 318 } 319 320 func (m *mpStub) AverageEntryPrice() *num.Uint { 321 return num.UintZero() 322 } 323 324 func (m *mpStub) Transfer() *types.Transfer { 325 ret := &types.Transfer{ 326 Owner: m.party, 327 Amount: &types.FinancialAmount{ 328 Asset: "testasset", 329 Amount: m.amount.Clone(), 330 }, 331 Type: types.TransferTypePerpFundingWin, 332 MinAmount: num.UintZero(), 333 Market: "market", 334 TransferID: ptr.From("test"), 335 } 336 if m.loss { 337 ret.Type = types.TransferTypePerpFundingLoss 338 } 339 return ret 340 } 341 342 func getTransferEvent(party, market string, amount *num.Uint, loss bool) events.Transfer { 343 if amount == nil { 344 amount = num.UintZero() 345 } 346 mp := mpStub{ 347 party: party, 348 market: market, 349 amount: amount.Clone(), 350 loss: loss, 351 } 352 return &mp 353 }