Skip to content

Commit

Permalink
Merge branch 'master' into releases/v0.21.x
Browse files Browse the repository at this point in the history
  • Loading branch information
k-yang committed Jun 30, 2023
2 parents 4cb48d3 + c1ee1a9 commit c4efa48
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 171 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Improvements

* [#1462](https://github.com/NibiruChain/nibiru/pull/1462) - fix(perp): Add pair to liquidation failed event.
* [#1424](https://github.com/NibiruChain/nibiru/pull/1424) - feat(perp): Add change type and exchanged margin to position changed events.
* [#1390](https://github.com/NibiruChain/nibiru/pull/1390) - fix(localnet.sh): Fix genesis market initialization + add force exits on failure
* [#1340](https://github.com/NibiruChain/nibiru/pull/1340) - feat(wasm): Enforce x/sudo contract permission checks on the shifter contract + integration tests
Expand Down Expand Up @@ -600,4 +601,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Testing

* [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
7 changes: 6 additions & 1 deletion proto/nibiru/perp/v2/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ message MsgMultiLiquidateResponse {
// nullable since no fee is taken on failed liquidation

string trader = 5;
string pair = 6 [
(gogoproto.customtype) =
"github.com/NibiruChain/nibiru/x/common/asset.Pair",
(gogoproto.nullable) = false
];
}

repeated LiquidationResponse liquidations = 1;
Expand Down Expand Up @@ -266,4 +271,4 @@ message MsgDonateToEcosystemFund {
];
}

message MsgDonateToEcosystemFundResponse {}
message MsgDonateToEcosystemFundResponse {}
38 changes: 38 additions & 0 deletions x/common/testutil/events.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package testutil

import (
"fmt"
"reflect"
"strings"

"encoding/json"

"github.com/cosmos/gogoproto/proto"

"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"

abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -59,3 +66,34 @@ func RequireContainsTypedEvent(t require.TestingT, ctx sdk.Context, event proto.

t.Errorf("event not found, event: %+v, found events: %+v", event, foundEvents)
}

// ProtoToJson converts a proto message into a JSON string using the proto codec.
// A codec defines a functionality for serializing other objects. The proto
// codec provides full Protobuf serialization compatibility.
func ProtoToJson(protoMsg proto.Message) (jsonOut string, err error) {
protoCodec := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
var jsonBz json.RawMessage
jsonBz, err = protoCodec.MarshalJSON(protoMsg)
return string(jsonBz), err
}

// EventHasAttribueValue parses the given ABCI event at a key to see if it
// matches (contains) the wanted value.
//
// Args:
// - abciEvent: The event under test
// - key: The key for which we'll check the value
// - want: The desired value
func EventHasAttribueValue(abciEvent sdk.Event, key string, want string) error {
attr, ok := abciEvent.GetAttribute(key)
if !ok {
return fmt.Errorf("abci event does not contain key: %s", key)
}

got := attr.Value
if !strings.Contains(got, want) {
return fmt.Errorf("expected %s %s, got %s", key, want, got)
}

return nil
}
201 changes: 141 additions & 60 deletions x/perp/v2/integration/assertion/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,93 +10,174 @@ import (
"github.com/gogo/protobuf/proto"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/x/common/testutil"
"github.com/NibiruChain/nibiru/x/common/testutil/action"
types "github.com/NibiruChain/nibiru/x/perp/v2/types"
)

type positionChangedEventShouldBeEqual struct {
ExpectedEvent *types.PositionChangedEvent
var _ action.Action = (*containsLiquidateEvent)(nil)
var _ action.Action = (*positionChangedEventShouldBeEqual)(nil)

// TODO test(perp): Add action for testing the appearance of of successful
// liquidation events.

// PositionChangedEventShouldBeEqual checks that the position changed event is
// equal to the expected event.
func PositionChangedEventShouldBeEqual(
expectedEvent *types.PositionChangedEvent,
) action.Action {
return positionChangedEventShouldBeEqual{
ExpectedEvent: expectedEvent,
}
}

func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) {
for _, abciEvent := range ctx.EventManager().Events() {
if abciEvent.Type != proto.MessageName(p.ExpectedEvent) {
continue
}
typedEvent, err := sdk.ParseTypedEvent(abci.Event{
Type: abciEvent.Type,
Attributes: abciEvent.Attributes,
})
if err != nil {
return ctx, err, false
}
// ContainsLiquidateEvent checks if a typed event (proto.Message) is contained in the
// event manager of the app context.
func ContainsLiquidateEvent(
expectedEvent types.LiquidationFailedEvent,
) action.Action {
return containsLiquidateEvent{
ExpectedEvent: expectedEvent,
}
}

theEvent, ok := typedEvent.(*types.PositionChangedEvent)
if !ok {
return ctx, fmt.Errorf("expected event is not of type PositionChangedEvent"), false
}
// eventEquals exports functions for comparing sdk.Events to concrete typed
// events implemented as proto.Message instances in Nibiru.
var eventEquals = iEventEquals{}

if err := types.PositionsAreEqual(&p.ExpectedEvent.FinalPosition, &theEvent.FinalPosition); err != nil {
return ctx, err, false
}
type iEventEquals struct{}

fieldErrs := []string{}
if !theEvent.PositionNotional.Equal(p.ExpectedEvent.PositionNotional) {
err := fmt.Errorf("expected position notional %s, got %s", p.ExpectedEvent.PositionNotional, theEvent.PositionNotional)
fieldErrs = append(fieldErrs, err.Error())
}
// --------------------------------------------------
// --------------------------------------------------

if !theEvent.TransactionFee.Equal(p.ExpectedEvent.TransactionFee) {
err := fmt.Errorf("expected transaction fee %s, got %s", p.ExpectedEvent.TransactionFee, theEvent.TransactionFee)
fieldErrs = append(fieldErrs, err.Error())
}
type containsLiquidateEvent struct {
ExpectedEvent types.LiquidationFailedEvent
}

if !theEvent.RealizedPnl.Equal(p.ExpectedEvent.RealizedPnl) {
err := fmt.Errorf("expected realized pnl %s, got %s", p.ExpectedEvent.RealizedPnl, theEvent.RealizedPnl)
fieldErrs = append(fieldErrs, err.Error())
func (act containsLiquidateEvent) Do(_ *app.NibiruApp, ctx sdk.Context) (
outCtx sdk.Context, err error, isMandatory bool,
) {
wantEvent := act.ExpectedEvent
isEventContained := false
events := ctx.EventManager().Events()
eventsOfMatchingType := []abci.Event{}
for idx, sdkEvent := range events {
err := eventEquals.LiquidationFailedEvent(sdkEvent, wantEvent, idx)
if err == nil {
isEventContained = true
break
} else if sdkEvent.Type != "nibiru.perp.v2.LiquidationFailedEvent" {
continue
} else if sdkEvent.Type == "nibiru.perp.v2.LiquidationFailedEvent" && err != nil {
abciEvent := abci.Event{
Type: sdkEvent.Type,
Attributes: sdkEvent.Attributes,
}
eventsOfMatchingType = append(eventsOfMatchingType, abciEvent)
}
}

if isEventContained {
// happy path
return ctx, nil, true
} else {
// Show descriptive error messages if the expected event is missing
wantEventJson, _ := testutil.ProtoToJson(&wantEvent)
var matchingEvents string = sdk.StringifyEvents(eventsOfMatchingType).String()
return ctx, errors.New(
strings.Join([]string{
fmt.Sprintf("expected the context event manager to contain event: %s.", wantEventJson),
fmt.Sprintf("found %v events:", len(events)),
fmt.Sprintf("events of matching type:\n%v", matchingEvents),
}, "\n"),
), false
}
}

if !theEvent.BadDebt.Equal(p.ExpectedEvent.BadDebt) {
err := fmt.Errorf("expected bad debt %s, got %s", p.ExpectedEvent.BadDebt, theEvent.BadDebt)
func (ee iEventEquals) LiquidationFailedEvent(
sdkEvent sdk.Event, tevent types.LiquidationFailedEvent, eventIdx int,
) error {
fieldErrs := []string{fmt.Sprintf("[DEBUG eventIdx: %v]", eventIdx)}

for _, keyWantPair := range []struct {
key string
want string
}{
{"pair", tevent.Pair.String()},
{"trader", tevent.Trader},
{"liquidator", tevent.Liquidator},
{"reason", tevent.Reason.String()},
} {
if err := testutil.EventHasAttribueValue(sdkEvent, keyWantPair.key, keyWantPair.want); err != nil {
fieldErrs = append(fieldErrs, err.Error())
}
}

if !theEvent.FundingPayment.Equal(p.ExpectedEvent.FundingPayment) {
err := fmt.Errorf("expected funding payment %s, got %s", p.ExpectedEvent.FundingPayment, theEvent.FundingPayment)
if len(fieldErrs) != 1 {
return errors.New(strings.Join(fieldErrs, ". "))
}
return nil
}

func (ee iEventEquals) PositionChangedEvent(
sdkEvent sdk.Event, tevent types.PositionChangedEvent, eventIdx int,
) error {
fieldErrs := []string{fmt.Sprintf("[DEBUG eventIdx: %v]", eventIdx)}

for _, keyWantPair := range []struct {
key string
want string
}{
{"position_notional", tevent.PositionNotional.String()},
{"transaction_fee", tevent.TransactionFee.String()},
{"bad_debt", tevent.BadDebt.String()},
{"realized_pnl", tevent.RealizedPnl.String()},
{"funding_payment", tevent.FundingPayment.String()},
{"block_height", fmt.Sprintf("%v", tevent.BlockHeight)},
{"margin_to_user", tevent.MarginToUser.String()},
{"change_reason", string(tevent.ChangeReason)},
} {
if err := testutil.EventHasAttribueValue(sdkEvent, keyWantPair.key, keyWantPair.want); err != nil {
fieldErrs = append(fieldErrs, err.Error())
}
}

if theEvent.BlockHeight != p.ExpectedEvent.BlockHeight {
err := fmt.Errorf("expected block height %d, got %d", p.ExpectedEvent.BlockHeight, theEvent.BlockHeight)
fieldErrs = append(fieldErrs, err.Error())
if len(fieldErrs) != 1 {
return errors.New(strings.Join(fieldErrs, ". "))
}
return nil
}

type positionChangedEventShouldBeEqual struct {
ExpectedEvent *types.PositionChangedEvent
}

func (p positionChangedEventShouldBeEqual) Do(_ *app.NibiruApp, ctx sdk.Context) (sdk.Context, error, bool) {
for eventIdx, gotSdkEvent := range ctx.EventManager().Events() {
if gotSdkEvent.Type != proto.MessageName(p.ExpectedEvent) {
continue
}
gotProtoMessage, err := sdk.ParseTypedEvent(abci.Event{
Type: gotSdkEvent.Type,
Attributes: gotSdkEvent.Attributes,
})
if err != nil {
return ctx, err, false
}

if !theEvent.MarginToUser.Equal(p.ExpectedEvent.MarginToUser) {
err := fmt.Errorf("expected exchanged margin %s, got %s",
p.ExpectedEvent.MarginToUser, theEvent.MarginToUser)
fieldErrs = append(fieldErrs, err.Error())
gotTypedEvent, ok := gotProtoMessage.(*types.PositionChangedEvent)
if !ok {
return ctx, fmt.Errorf("expected event is not of type PositionChangedEvent"), false
}

if theEvent.ChangeReason != p.ExpectedEvent.ChangeReason {
err := fmt.Errorf("expected change type %s, got %s",
p.ExpectedEvent.ChangeReason, theEvent.ChangeReason)
fieldErrs = append(fieldErrs, err.Error())
if err := types.PositionsAreEqual(&p.ExpectedEvent.FinalPosition, &gotTypedEvent.FinalPosition); err != nil {
return ctx, err, false
}

if len(fieldErrs) != 0 {
err := strings.Join(fieldErrs, "\n")
return ctx, errors.New(err), false
if err := eventEquals.PositionChangedEvent(gotSdkEvent, *gotTypedEvent, eventIdx); err != nil {
return ctx, err, false
}
}

return ctx, nil, false
}

// PositionChangedEventShouldBeEqual checks that the position changed event is equal to the expected event.
func PositionChangedEventShouldBeEqual(
expectedEvent *types.PositionChangedEvent,
) action.Action {
return positionChangedEventShouldBeEqual{
ExpectedEvent: expectedEvent,
}
}
Loading

0 comments on commit c4efa48

Please sign in to comment.