diff --git a/x/exchange/keeper/fulfillment_test.go b/x/exchange/keeper/fulfillment_test.go index 98eb009a4b..baabc925d1 100644 --- a/x/exchange/keeper/fulfillment_test.go +++ b/x/exchange/keeper/fulfillment_test.go @@ -1,8 +1,11 @@ package keeper_test import ( + "github.com/gogo/protobuf/proto" + sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/provenance-io/provenance/x/exchange" ) @@ -20,7 +23,7 @@ func (s *TestSuite) TestKeeper_FillBids() { expHoldCalls HoldCalls expBankCalls BankCalls }{ - // tests on error conditions. + // Tests on error conditions. { name: "invalid msg", msg: exchange.MsgFillBidsRequest{ @@ -447,6 +450,7 @@ func (s *TestSuite) TestKeeper_FillBids() { }, }, + // Tests on successes. { name: "one order: no fees", setup: func() { @@ -664,7 +668,7 @@ func (s *TestSuite) TestKeeper_FillAsks() { expHoldCalls HoldCalls expBankCalls BankCalls }{ - // tests on error conditions. + // Tests on error conditions. { name: "invalid msg", msg: exchange.MsgFillAsksRequest{ @@ -1096,6 +1100,7 @@ func (s *TestSuite) TestKeeper_FillAsks() { }, }, + // Tests on successes. { name: "one order: no fees", setup: func() { @@ -1301,4 +1306,580 @@ func (s *TestSuite) TestKeeper_FillAsks() { } } -// TODO[1658]: func (s *TestSuite) TestKeeper_SettleOrders() +func (s *TestSuite) TestKeeper_SettleOrders() { + tests := []struct { + name string + bankKeeper *MockBankKeeper + holdKeeper *MockHoldKeeper + setup func() + marketID uint32 + askOrderIDs []uint64 + bidOrderIDs []uint64 + expectPartial bool + expErr string + expEvents []proto.Message + expPartialLeft *exchange.Order + expHoldCalls HoldCalls + expBankCalls BankCalls + }{ + // Tests on error conditions. + { + name: "market does not exist", + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{2}, + expectPartial: false, + expErr: "market 1 does not exist", + }, + { + name: "errors getting orders", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Buyer: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 2, Seller: s.addr2.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Seller: s.addr3.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(6).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 3, Buyer: s.addr4.String(), + })) + }, + marketID: 1, + askOrderIDs: []uint64{1, 2, 3}, + bidOrderIDs: []uint64{4, 5, 6}, + expectPartial: false, + expErr: s.joinErrs( + "order 1 not found", + "order 2 is type bid: expected ask", + "order 3 market id 2 does not equal requested market id 1", + "order 4 not found", + "order 5 is type ask: expected bid", + "order 6 market id 3 does not equal requested market id 1", + ), + }, + { + name: "errors building settlement", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("2acorn"), Price: s.coin("5plum"), MarketId: 1, Buyer: s.addr2.String(), + })) + }, + marketID: 1, + askOrderIDs: []uint64{3}, + bidOrderIDs: []uint64{2}, + expectPartial: false, + expErr: s.joinErrs( + "cannot settle different ask \"1apple\" and bid \"2acorn\" asset denoms", + "cannot settle different ask \"6peach\" and bid \"5plum\" price denoms", + ), + }, + { + name: "expect partial, full", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Buyer: s.addr2.String(), + })) + }, + marketID: 1, + askOrderIDs: []uint64{3}, + bidOrderIDs: []uint64{2}, + expectPartial: true, + expErr: "settlement unexpectedly resulted in all orders fully filled", + }, + { + name: "expect full, partial", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6peach"), MarketId: 1, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("2apple"), Price: s.coin("12peach"), MarketId: 1, Buyer: s.addr2.String(), + AllowPartial: true, + })) + }, + marketID: 1, + askOrderIDs: []uint64{3}, + bidOrderIDs: []uint64{2}, + expectPartial: false, + expErr: "settlement resulted in unexpected partial order 2", + }, + { + name: "errors releasing holds", + holdKeeper: NewMockHoldKeeper(). + WithReleaseHoldResults("first hold error", "second error releasing hold", "hold error the third"), + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("4apple"), Price: s.coin("16peach"), MarketId: 1, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("2apple"), Price: s.coin("8peach"), MarketId: 1, Buyer: s.addr2.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(4).WithBid(&exchange.BidOrder{ + Assets: s.coin("3apple"), Price: s.coin("12peach"), MarketId: 1, Buyer: s.addr3.String(), + AllowPartial: true, + })) + }, + marketID: 1, + askOrderIDs: []uint64{3}, + bidOrderIDs: []uint64{2, 4}, + expectPartial: true, + expErr: s.joinErrs( + "error releasing hold for ask order 3: first hold error", + "error releasing hold for bid order 2: second error releasing hold", + "error releasing hold for bid order 4: hold error the third", + ), + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr1, funds: s.coins("4apple")}, + {addr: s.addr2, funds: s.coins("8peach")}, + {addr: s.addr3, funds: s.coins("8peach")}, + }, + }, + }, + { + name: "errors transferring stuff", + bankKeeper: NewMockBankKeeper(). + WithSendCoinsResults("first send error", "second send error"). + WithSendCoinsFromAccountToModuleResults("and a fee collection error too"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 1000, + DenomSplits: []exchange.DenomSplit{{Denom: "grape", Split: 5000}}, + }) + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("4apple"), Price: s.coin("16peach"), MarketId: 1, Seller: s.addr1.String(), + SellerSettlementFlatFee: s.coinP("100fig"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("4apple"), Price: s.coin("16peach"), MarketId: 1, Buyer: s.addr2.String(), + BuyerSettlementFees: s.coins("50grape"), + })) + }, + marketID: 1, + askOrderIDs: []uint64{3}, + bidOrderIDs: []uint64{2}, + expectPartial: false, + expErr: s.joinErrs( + "first send error", + "second send error", + "error collecting exchange fee 10fig,25grape (based off 100fig,50grape) from market 1: "+ + "and a fee collection error too", + ), + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr1, funds: s.coins("100fig,4apple")}, + {addr: s.addr2, funds: s.coins("50grape,16peach")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr2, amt: s.coins("4apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr1, amt: s.coins("16peach")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: false, + inputs: []banktypes.Input{ + {Address: s.addr1.String(), Coins: s.coins("100fig")}, + {Address: s.addr2.String(), Coins: s.coins("50grape")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr1.String(), Coins: s.coins("100fig,50grape")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + { + senderAddr: s.marketAddr1, + recipientModule: s.feeCollector, + amt: s.coins("10fig,25grape"), + }, + }, + }, + }, + { + name: "error updating partial", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("5peach"), MarketId: 1, Buyer: s.addr4.String(), + ExternalId: "oops-dup-id", + })) + key8, value8, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(8).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr5.String(), + Assets: s.coin("10apple"), + Price: s.coin("50peach"), + AllowPartial: true, + ExternalId: "oops-dup-id", + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 8") + store.Set(key8, value8) + }, + marketID: 1, + askOrderIDs: []uint64{8}, + bidOrderIDs: []uint64{5}, + expectPartial: true, + expErr: "could not update partial ask order 8: external id \"oops-dup-id\" is " + + "already in use by order 5: cannot be used for order 8", + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr4, funds: s.coins("5peach")}, + {addr: s.addr5, funds: s.coins("1apple")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr5, amt: s.coins("5peach")}, + }, + }, + }, + + // Tests on successes. + { + name: "one ask one bid: both full, no fees", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("5peach"), MarketId: 1, Seller: s.addr3.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("5peach"), MarketId: 1, Buyer: s.addr4.String(), + })) + }, + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{5}, + expectPartial: false, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{OrderId: 1, Assets: "1apple", Price: "5peach", MarketId: 1}, + &exchange.EventOrderFilled{OrderId: 5, Assets: "1apple", Price: "5peach", MarketId: 1}, + }, + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr3, funds: s.coins("1apple")}, + {addr: s.addr4, funds: s.coins("5peach")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr3, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr3, amt: s.coins("5peach")}, + }, + }, + }, + { + name: "one ask one bid: both full, all the fees", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 1000}) + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + FeeSellerSettlementRatios: s.ratios("25peach:1peach"), + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("10apple"), Price: s.coin("50peach"), MarketId: 1, Seller: s.addr3.String(), + SellerSettlementFlatFee: s.coinP("3peach"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + Assets: s.coin("10apple"), Price: s.coin("50peach"), MarketId: 1, Buyer: s.addr4.String(), + BuyerSettlementFees: s.coins("15peach"), + })) + }, + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{5}, + expectPartial: false, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{OrderId: 1, Assets: "10apple", Price: "50peach", MarketId: 1, Fees: "5peach"}, + &exchange.EventOrderFilled{OrderId: 5, Assets: "10apple", Price: "50peach", MarketId: 1, Fees: "15peach"}, + }, + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr3, funds: s.coins("10apple")}, + {addr: s.addr4, funds: s.coins("65peach")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr3, toAddr: s.addr4, amt: s.coins("10apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr3, amt: s.coins("50peach")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + inputs: []banktypes.Input{ + {Address: s.addr3.String(), Coins: s.coins("5peach")}, + {Address: s.addr4.String(), Coins: s.coins("15peach")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr1.String(), Coins: s.coins("20peach")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr1, recipientModule: s.feeCollector, amt: s.coins("2peach")}, + }, + }, + }, + { + name: "one ask one bid: partial ask", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("10apple"), Price: s.coin("50peach"), MarketId: 1, Seller: s.addr5.String(), + SellerSettlementFlatFee: s.coinP("20fig"), + ExternalId: "the-ask-order", + AllowPartial: true, + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("7apple"), Price: s.coin("40peach"), MarketId: 1, Buyer: s.addr3.String(), + ExternalId: "the-bid-order", + })) + }, + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{2}, + expectPartial: true, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{ + OrderId: 2, Assets: "7apple", Price: "40peach", + MarketId: 1, ExternalId: "the-bid-order", + }, + &exchange.EventOrderPartiallyFilled{ + OrderId: 1, Assets: "7apple", Price: "40peach", Fees: "14fig", + MarketId: 1, ExternalId: "the-ask-order", + }, + }, + expPartialLeft: exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("3apple"), Price: s.coin("15peach"), MarketId: 1, Seller: s.addr5.String(), + SellerSettlementFlatFee: s.coinP("6fig"), + ExternalId: "the-ask-order", + AllowPartial: true, + }), + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr3, funds: s.coins("40peach")}, + {addr: s.addr5, funds: s.coins("14fig,7apple")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr3, amt: s.coins("7apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr3, toAddr: s.addr5, amt: s.coins("40peach")}, + {fromAddr: s.addr5, toAddr: s.marketAddr1, amt: s.coins("14fig")}, + }, + }, + }, + { + name: "one ask one bid: partial bid", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("7apple"), Price: s.coin("30peach"), MarketId: 1, Seller: s.addr5.String(), + ExternalId: "the-ask-order", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("10apple"), Price: s.coin("50peach"), MarketId: 1, Buyer: s.addr3.String(), + BuyerSettlementFees: s.coins("20fig"), + ExternalId: "the-bid-order", + AllowPartial: true, + })) + }, + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{2}, + expectPartial: true, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{ + OrderId: 1, Assets: "7apple", Price: "35peach", + MarketId: 1, ExternalId: "the-ask-order", + }, + &exchange.EventOrderPartiallyFilled{ + OrderId: 2, Assets: "7apple", Price: "35peach", Fees: "14fig", + MarketId: 1, ExternalId: "the-bid-order", + }, + }, + expPartialLeft: exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("3apple"), Price: s.coin("15peach"), MarketId: 1, Buyer: s.addr3.String(), + BuyerSettlementFees: s.coins("6fig"), + ExternalId: "the-bid-order", + AllowPartial: true, + }), + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr5, funds: s.coins("7apple")}, + {addr: s.addr3, funds: s.coins("14fig,35peach")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr3, amt: s.coins("7apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr3, toAddr: s.addr5, amt: s.coins("35peach")}, + {fromAddr: s.addr3, toAddr: s.marketAddr1, amt: s.coins("14fig")}, + }, + }, + }, + { + name: "two asks, three bids", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 2}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("25apple"), Price: s.coin("100peach"), MarketId: 2, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(6).WithBid(&exchange.BidOrder{ + Assets: s.coin("20apple"), Price: s.coin("40peach"), MarketId: 2, Buyer: s.addr2.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithBid(&exchange.BidOrder{ + Assets: s.coin("30apple"), Price: s.coin("60peach"), MarketId: 2, Buyer: s.addr3.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(77).WithAsk(&exchange.AskOrder{ + Assets: s.coin("75apple"), Price: s.coin("50peach"), MarketId: 2, Seller: s.addr4.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(88).WithBid(&exchange.BidOrder{ + Assets: s.coin("50apple"), Price: s.coin("50peach"), MarketId: 2, Buyer: s.addr5.String(), + })) + }, + marketID: 2, + askOrderIDs: []uint64{77, 1}, + bidOrderIDs: []uint64{7, 6, 88}, + expectPartial: false, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{OrderId: 77, Assets: "75apple", Price: "50peach", MarketId: 2}, + &exchange.EventOrderFilled{OrderId: 1, Assets: "25apple", Price: "100peach", MarketId: 2}, + &exchange.EventOrderFilled{OrderId: 7, Assets: "30apple", Price: "60peach", MarketId: 2}, + &exchange.EventOrderFilled{OrderId: 6, Assets: "20apple", Price: "40peach", MarketId: 2}, + &exchange.EventOrderFilled{OrderId: 88, Assets: "50apple", Price: "50peach", MarketId: 2}, + }, + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr4, funds: s.coins("75apple")}, + {addr: s.addr1, funds: s.coins("25apple")}, + {addr: s.addr3, funds: s.coins("60peach")}, + {addr: s.addr2, funds: s.coins("40peach")}, + {addr: s.addr5, funds: s.coins("50peach")}, + }, + }, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr5, amt: s.coins("25apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr1, amt: s.coins("40peach")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr1, amt: s.coins("50peach")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{ + {Address: s.addr4.String(), Coins: s.coins("75apple")}, + }, + outputs: []banktypes.Output{ + {Address: s.addr3.String(), Coins: s.coins("30apple")}, + {Address: s.addr2.String(), Coins: s.coins("20apple")}, + {Address: s.addr5.String(), Coins: s.coins("25apple")}, + }, + }, + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{ + {Address: s.addr3.String(), Coins: s.coins("60peach")}, + }, + outputs: []banktypes.Output{ + {Address: s.addr4.String(), Coins: s.coins("50peach")}, + {Address: s.addr1.String(), Coins: s.coins("10peach")}, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if s.accKeeper == nil { + s.accKeeper = NewMockAccountKeeper() + } + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + + expEvents := make(sdk.Events, len(tc.expEvents)) + for i, expEvent := range tc.expEvents { + event, err := sdk.TypedEventToEvent(expEvent) + s.Require().NoError(err, "[%d]TypedEventToEvent(%T)", expEvent) + expEvents[i] = event + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + kpr := s.k.WithAccountKeeper(s.accKeeper).WithBankKeeper(tc.bankKeeper).WithHoldKeeper(tc.holdKeeper) + var err error + testFunc := func() { + err = kpr.SettleOrders(ctx, tc.marketID, tc.askOrderIDs, tc.bidOrderIDs, tc.expectPartial) + } + s.Require().NotPanics(testFunc, "SettleOrders") + s.assertErrorValue(err, tc.expErr, "SettleOrders error") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "SettleOrders events") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "SettleOrders") + s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "SettleOrders") + + if len(actEvents) == 0 { + return + } + + for _, orderID := range tc.askOrderIDs { + if tc.expPartialLeft == nil || tc.expPartialLeft.OrderId != orderID { + order, oerr := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(oerr, "GetOrder(%d) (ask) after SettleOrders", orderID) + s.Assert().Nil(order, "GetOrder(%d) (ask) after SettleOrders", orderID) + } + } + for _, orderID := range tc.bidOrderIDs { + if tc.expPartialLeft == nil || tc.expPartialLeft.OrderId != orderID { + order, oerr := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(oerr, "GetOrder(%d) (bid) after SettleOrders", orderID) + s.Assert().Nil(order, "GetOrder(%d) (bid) after SettleOrders", orderID) + } + } + if tc.expPartialLeft != nil { + order, oerr := s.k.GetOrder(s.ctx, tc.expPartialLeft.OrderId) + s.Assert().NoError(oerr, "GetOrder(%d) (partial) after SettleOrders", tc.expPartialLeft.OrderId) + s.Assert().Equal(tc.expPartialLeft, order, "GetOrder(%d) (partial) after SettleOrders", tc.expPartialLeft.OrderId) + } + }) + } +}