From 8142550910c3f64f216bceb31c12921a954ccd17 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Wed, 1 Nov 2023 14:13:58 -0600 Subject: [PATCH] Dwedul/1699 exchange keeper tests (#1713) * [1699]: Unit tests on params. * [1699]: Unit tests on the flat fee getters. * [1699]: Unit tests on the ratio fee getters and calculators. * [1699]: Unit tests on the flat fee validators. * [1699]: Unit tests on ValidateAskPrice. * [1699]: Change TestSuite.ratio to use ParseFeeRatio. * [1699]: Unit tests on ValidateBuyerSettlementFee. * [1699]: Unit tests on UpdateFees. * [1699]: Create Keeper.IsMarketKnown and tweak the comments on IsMarketActive. * [1699]: Unit tests on IsMarketKnown and IsMarketActive. * [1699]: Unit tests on UpdateMarketActive. * [1699]: Unit tests 0on IsUserSettlementAllowed and UpdateUserSettlementAllowed. * [1699]: Unit tests on HasPermission and each of the specific Can... permission checkers. * [1699]: Fix getAccessGrants. * [1699]: Unit tests on GetUserPermissions and GetAccessGrants. * [1699]: Unit tests on UpdatePermissions. * [1699]: Unit tests on GetReqAttrsAsk, GetReqAttrsBid, CanCreateAsk, and CanCreateBid. * [1699]: Make NewEventMarketPermissionsUpdated just take in the admin as a string since that's what we've got it as when we want to create that event. * [1699]: Make NewEventMarketReqAttrUpdated just take in the admin as a string since that's what we've got it as when we want to create that event. * [1699]: Unit tests on UpdateReqAttrs. * [1699]: Unit tests on GetMarketAccount. * [1699]: Unit tests on GetMarketDetails. * [1699]: Remove 'Calls' from the field names of the mock keeper ...Calls structs. * [1699]: Update NewEventMarketDetailsUpdated to take in a string since that's what we've already got it as everywhere. * [1699]: Update NewEventMarketActiveUpdated, NewEventMarketEnabled, and NewEventMarketDisabled to take in an updatedBy string (instead of AccAddress) since that's what we've already got it as anyway. * [1699]: Change NewEventMarketWithdraw to take in withdrawnBy as a string since we don't need it as an addr anywhere. * [1699]: Update NewEventMarketUserSettleUpdated, NewEventMarketUserSettleEnabled, and NewEventMarketUserSettleDisabled to take in updatedBy as a string. * [1699]: Update exchange.NewEventOrderCancelled to take in cancelledBy as a string. * [1699]: A little cleanup in TestTypedEventToEvent. * [1699]: Unit tests on UpdateMarketDetails. * [1699]: Change NewAccountResultsMap to NewAccountModifierMap and have NewAccount modify the provided account directly since that's how that func is used. * [1699]: Unit tests on CreateMarket. * [1699]: Unit tests on GetMarket and IterateMarkets. * [1699]: A little cleanup in TestKeeper_CreateMarket since I have SetAccount update the GetAccount results in the mock account keeper. * [1699]: Unit tests on GetMarketBrief. * [1699]: Unit tests on WithdrawMarketFunds. * [1699]: Unit tests on ValidateMarket. * [1699]: Clean up the market unit tests by not passing the *TestSuite around needlessly. * [1699] Unit tests on GetOrder. * [1699]: Unit tests on GetOrderByExternalID. * [1699]: Fix setOrderInStore to check the store correctly for an existing external id. * [1699]: Add IsReqAttrMatch unit test case. * [1699]: Unit tests on CreateAskOrder and CreateBidOrder. * [1699]: Unit tests on CancelOrder. * [1699]: Unit tests on SetOrderExternalID. * [1699]: In parseOrderStoreKeyValue, include the failed order id when it's available. * [1699]: Unit tests on IterateOrders. * [1699]: Unit tests on IterateMarketOrders, IterateAddressOrders, and IterateAssetOrders. * [1699]: In InitGenesis, ensure that the last order id is at least the largest order id, and include the hold error in the returned error. * [1699]: Unit tests on InitGenesis and ExportGenesis. * [1699]: Create expEvents using var instead of empty slice since assertEqualEvents doesn't care about nil vs empty. Call assertEqualEvents in TestKeeper_SetOrderExternalID. * [1699]: Unit tests on FillBids. * [1699]: Unit tests on FillAsks. * [1699]: Unit tests on SettleOrders. * [1699]: Take the stdlibCtx out of the TestSuite and delete the unused int and intStr methods. Replace assertErrorContents with assertErrorContentsf. * [1699]: In OrderFeeCalc, return an error if the market does not exist. * [1699]: Unit tests on the OrderFeeCalc query. * [1699]: In GetOrderByExternalID, return an error if either the market is zero or no external id is given. * [1699]: Unit tests on GetOrder and GetOrderByExternalID. * [1699]: Fix the order index queries to properly get the orders. * [1699]: Modify filteredPaginateAfterOrder with key to return a NextKey that's a hit instead of just the next key the iterator sees. This way, it matches the NextKey behavior when using Offset instead. * [1699]: In getOrderIterator, for the reverse iterator, the 'start' orderID should be one more than the afterOrderID. * [1699]: Unit tests on the GetMarketOrders query. * [1699]: Include the invalid owner in the error from GetOwnerOrders. * [1699]: Unit tests on the GetOwnerOrders query. * [1699]: Unit tests on the GetAssetOrders query. * [1699]: Add a unit test on each of the order iterator queries for a case when the page request is invalid. * [1699]: Fix GetAllOrders. * [1699]: Unit tests on GetAllOrders. * [1699]: Change the querySetupFunc to not take in the context, and just do a swaparoo on the context in the test suite. * [1699]: Set the address in the QueryGetMarketResponse. * [1699]: Unit tests on the GetMarket query. Create requireCreateMarketUnmocked and use that everywhere in the query tests. * [1699]: Unit tests on the GetAllMarkets query. * [1699]: Unit tests on the Params query. * [1699]: Unit tests on the ValidateCreateMarket query. * [1699]: Unit tests on the ValidateMarket query. * [1699]: Unit tests on the ValidateManageFees query. * [1699]: Remove todo about discussing the DefaultDefaultSplit value. Best consensus was that 5% seems alright until we know better. * [1699]: Comment change on the querySetupFunc definition. * [1699]: Unit tests on the CreateAsk and CreateBid endpoints. * [1699]: Unit tests on the CancelOrder endpoint. * [1699]: change IsMarketActive to only return true if the market is both known and active. * [1699]: Add saffron-rc2 no-op upgrade entry. * [1699]: In FillBids, calculate the seller settlement ratio fee off the total price instead of each order individually. * [1699]: Unit tests on FillBids. Extract the balance checking stuff so it's easier for several tests to use. * [1699]: Unit tests on FillAsks. * [1699]: Unit tests on the MarketSettle endpoint. * [1699]: Fix error message from MarketSetOrderExternalID when admin doesn't have permission since they might not actually be uuids. * [1699]: Unit tests on the MarketSetOrderExternalID endpoint. * [1699] Unit tests on the MarketWithdraw endpoint. * [1699]: Unit tests on the MarketUpdateDetails endpoint. Replace uses of requireCreateMarket in the msg_server_test file (with requireCreateMarketUnmocked). * [1699]: Unit tests on the MarketUpdateEnabled and MarketUpdateUserSettle endpoints. * [1699]: Unit tests on the MarketManagePermissions and MarketManageReqAttrs endpoints. * [1699]: Unit tests on the GovCreateMarket, GovManageFees, and GovUpdateParams endpoints. All done with tests. * [1699]: Mark v1.17.0-rc1 in the changelog. * [1699]: Add changelog entry. * [1699]: clean up some of the expected event creation stuff. * [1699]: Move all the generic keeper test helper funcs to a new suite_test.go file as well as the TestSuite definition. Do a little cleanup to use those in a few places where it made sense. * [1699]: Remove some unneeded things from the TestSuite. * [1699]: Some comment clarifications. * [1699]: Make TestSuite.badKey for changing a key into a bad version of it for the tests that want an incorrect key entry. * [1699]: Do some of the market id comparisons as ints for better failure output. * [1699]: Refactor grpc_query_test to be similar to msg_server_test with the typed test definitions and test cases. * [1699]: Add some test cases to the ValidateManageFees query where everything in the msg is okay, but results in a bad market setup. * [1699]: Underscore some unused followup func arguments in msg_server_test. * [1699]: Create agCanOnly, agCanAllBut, and agCanEverything and use them in severl test cases where similar stuff was being done the hard way. * [1699]: Clean up some of the follow-up args type defs. --- CHANGELOG.md | 24 + app/upgrades.go | 1 + x/exchange/events.go | 40 +- x/exchange/events_test.go | 82 +- x/exchange/keeper/export_test.go | 64 +- x/exchange/keeper/fulfillment.go | 17 +- x/exchange/keeper/fulfillment_test.go | 1869 ++++++- x/exchange/keeper/genesis.go | 9 +- x/exchange/keeper/genesis_test.go | 500 +- x/exchange/keeper/grpc_query.go | 29 +- x/exchange/keeper/grpc_query_test.go | 4016 ++++++++++++++- x/exchange/keeper/keeper_test.go | 214 +- x/exchange/keeper/market.go | 47 +- x/exchange/keeper/market_test.go | 6742 ++++++++++++++++++++++++- x/exchange/keeper/mocks_test.go | 200 +- x/exchange/keeper/msg_server.go | 14 +- x/exchange/keeper/msg_server_test.go | 3271 +++++++++++- x/exchange/keeper/orders.go | 41 +- x/exchange/keeper/orders_test.go | 2953 ++++++++++- x/exchange/keeper/params_test.go | 330 +- x/exchange/keeper/suite_test.go | 670 +++ x/exchange/market_test.go | 6 + x/exchange/params.go | 1 - 23 files changed, 20619 insertions(+), 521 deletions(-) create mode 100644 x/exchange/keeper/suite_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f20259a75e..64f4f2a079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,26 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* Add the (empty) `saffron-rc2` upgrade [#1699](https://github.com/provenance-io/provenance/issues/1699). + +### Improvements + +* Wrote unit tests on the keeper methods [#1699](https://github.com/provenance-io/provenance/issues/1699). +* During `FillBids`, the seller settlement fee is now calculated on the total price instead of each order individually [#1699](https://github.com/provenance-io/provenance/issues/1699). +* In the `OrderFeeCalc` query, ensure the market exists [#1699](https://github.com/provenance-io/provenance/issues/1699). + +### Bug Fixes + +* During `InitGenesis`, ensure LastOrderId is at least the largest order id [#1699](https://github.com/provenance-io/provenance/issues/1699). +* Properly populate the permissions lists when reading access grants from state [#1699](https://github.com/provenance-io/provenance/issues/1699). +* Fixed the paginated order queries to properly look up orders [#1699](https://github.com/provenance-io/provenance/issues/1699). + +--- + +## [v1.17.0-rc1](https://github.com/provenance-io/provenance/releases/tag/v1.17.0-rc1) - 2023-10-18 + +### Features + * Create the `x/exchange` module which facilitates the buying and selling of assets [#1658](https://github.com/provenance-io/provenance/issues/1658). Assets and funds remain in their owner's account (with a hold on them) until the order is settled (or cancelled). Market's are created to manage order matching and define fees. @@ -118,6 +138,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ - Bump `golang.org/x/net` from 0.15.0 to 0.17.0 ([#1704](https://github.com/provenance-io/provenance/pull/1704)) - Bump `bufbuild/buf-lint-action` from 1.0.3 to 1.1.0 ([#1705](https://github.com/provenance-io/provenance/pull/1705)) +### Full Commit History + +* https://github.com/provenance-io/provenance/compare/v1.16.0...v1.17.0-rc1 + --- ## [v1.16.0](https://github.com/provenance-io/provenance/releases/tag/v1.16.0) - 2023-06-23 diff --git a/app/upgrades.go b/app/upgrades.go index 74db97d190..f1464af68c 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -120,6 +120,7 @@ var upgrades = map[string]appUpgrade{ }, Added: []string{icqtypes.ModuleName, oracletypes.ModuleName, ibchookstypes.StoreKey, hold.ModuleName, exchange.ModuleName}, }, + "saffron-rc2": {}, // upgrade for v1.17.0-rc2 "saffron": { // upgrade for v1.17.0, Handler: func(ctx sdk.Context, app *App, vm module.VersionMap) (module.VersionMap, error) { var err error diff --git a/x/exchange/events.go b/x/exchange/events.go index 2667ce70a4..2b07a81fff 100644 --- a/x/exchange/events.go +++ b/x/exchange/events.go @@ -15,10 +15,10 @@ func NewEventOrderCreated(order OrderI) *EventOrderCreated { } } -func NewEventOrderCancelled(order OrderI, cancelledBy sdk.AccAddress) *EventOrderCancelled { +func NewEventOrderCancelled(order OrderI, cancelledBy string) *EventOrderCancelled { return &EventOrderCancelled{ OrderId: order.GetOrderID(), - CancelledBy: cancelledBy.String(), + CancelledBy: cancelledBy, MarketId: order.GetMarketID(), ExternalId: order.GetExternalID(), } @@ -54,79 +54,79 @@ func NewEventOrderExternalIDUpdated(order OrderI) *EventOrderExternalIDUpdated { } } -func NewEventMarketWithdraw(marketID uint32, amount sdk.Coins, destination, withdrawnBy sdk.AccAddress) *EventMarketWithdraw { +func NewEventMarketWithdraw(marketID uint32, amount sdk.Coins, destination sdk.AccAddress, withdrawnBy string) *EventMarketWithdraw { return &EventMarketWithdraw{ MarketId: marketID, Amount: amount.String(), Destination: destination.String(), - WithdrawnBy: withdrawnBy.String(), + WithdrawnBy: withdrawnBy, } } -func NewEventMarketDetailsUpdated(marketID uint32, updatedBy sdk.AccAddress) *EventMarketDetailsUpdated { +func NewEventMarketDetailsUpdated(marketID uint32, updatedBy string) *EventMarketDetailsUpdated { return &EventMarketDetailsUpdated{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } // NewEventMarketActiveUpdated returns a new EventMarketEnabled if isActive == true, // or a new EventMarketDisabled if isActive == false. -func NewEventMarketActiveUpdated(marketID uint32, updatedBy sdk.AccAddress, isActive bool) proto.Message { +func NewEventMarketActiveUpdated(marketID uint32, updatedBy string, isActive bool) proto.Message { if isActive { return NewEventMarketEnabled(marketID, updatedBy) } return NewEventMarketDisabled(marketID, updatedBy) } -func NewEventMarketEnabled(marketID uint32, updatedBy sdk.AccAddress) *EventMarketEnabled { +func NewEventMarketEnabled(marketID uint32, updatedBy string) *EventMarketEnabled { return &EventMarketEnabled{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } -func NewEventMarketDisabled(marketID uint32, updatedBy sdk.AccAddress) *EventMarketDisabled { +func NewEventMarketDisabled(marketID uint32, updatedBy string) *EventMarketDisabled { return &EventMarketDisabled{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } // NewEventMarketUserSettleUpdated returns a new EventMarketUserSettleEnabled if isAllowed == true, // or a new EventMarketUserSettleDisabled if isActive == false. -func NewEventMarketUserSettleUpdated(marketID uint32, updatedBy sdk.AccAddress, isAllowed bool) proto.Message { +func NewEventMarketUserSettleUpdated(marketID uint32, updatedBy string, isAllowed bool) proto.Message { if isAllowed { return NewEventMarketUserSettleEnabled(marketID, updatedBy) } return NewEventMarketUserSettleDisabled(marketID, updatedBy) } -func NewEventMarketUserSettleEnabled(marketID uint32, updatedBy sdk.AccAddress) *EventMarketUserSettleEnabled { +func NewEventMarketUserSettleEnabled(marketID uint32, updatedBy string) *EventMarketUserSettleEnabled { return &EventMarketUserSettleEnabled{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } -func NewEventMarketUserSettleDisabled(marketID uint32, updatedBy sdk.AccAddress) *EventMarketUserSettleDisabled { +func NewEventMarketUserSettleDisabled(marketID uint32, updatedBy string) *EventMarketUserSettleDisabled { return &EventMarketUserSettleDisabled{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } -func NewEventMarketPermissionsUpdated(marketID uint32, updatedBy sdk.AccAddress) *EventMarketPermissionsUpdated { +func NewEventMarketPermissionsUpdated(marketID uint32, updatedBy string) *EventMarketPermissionsUpdated { return &EventMarketPermissionsUpdated{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } -func NewEventMarketReqAttrUpdated(marketID uint32, updatedBy sdk.AccAddress) *EventMarketReqAttrUpdated { +func NewEventMarketReqAttrUpdated(marketID uint32, updatedBy string) *EventMarketReqAttrUpdated { return &EventMarketReqAttrUpdated{ MarketId: marketID, - UpdatedBy: updatedBy.String(), + UpdatedBy: updatedBy, } } diff --git a/x/exchange/events_test.go b/x/exchange/events_test.go index 43a19626b4..211308e57b 100644 --- a/x/exchange/events_test.go +++ b/x/exchange/events_test.go @@ -93,16 +93,16 @@ func TestNewEventOrderCancelled(t *testing.T) { tests := []struct { name string order OrderI - cancelledBy sdk.AccAddress + cancelledBy string expected *EventOrderCancelled }{ { name: "ask order", order: NewOrder(11).WithAsk(&AskOrder{MarketId: 71, ExternalId: "an external identifier"}), - cancelledBy: sdk.AccAddress("CancelledBy_________"), + cancelledBy: "CancelledBy_________", expected: &EventOrderCancelled{ OrderId: 11, - CancelledBy: sdk.AccAddress("CancelledBy_________").String(), + CancelledBy: "CancelledBy_________", MarketId: 71, ExternalId: "an external identifier", }, @@ -110,10 +110,10 @@ func TestNewEventOrderCancelled(t *testing.T) { { name: "bid order", order: NewOrder(55).WithAsk(&AskOrder{MarketId: 88, ExternalId: "another external identifier"}), - cancelledBy: sdk.AccAddress("cancelled_by________"), + cancelledBy: "cancelled_by________", expected: &EventOrderCancelled{ OrderId: 55, - CancelledBy: sdk.AccAddress("cancelled_by________").String(), + CancelledBy: "cancelled_by________", MarketId: 88, ExternalId: "another external identifier", }, @@ -372,7 +372,7 @@ func TestNewEventMarketWithdraw(t *testing.T) { marketID := uint32(55) amountWithdrawn := sdk.NewCoins(sdk.NewInt64Coin("mine", 188382), sdk.NewInt64Coin("yours", 3)) destination := sdk.AccAddress("destination_________") - withdrawnBy := sdk.AccAddress("withdrawnBy_________") + withdrawnBy := sdk.AccAddress("withdrawnBy_________").String() var event *EventMarketWithdraw testFunc := func() { @@ -383,31 +383,31 @@ func TestNewEventMarketWithdraw(t *testing.T) { assert.Equal(t, marketID, event.MarketId, "MarketId") assert.Equal(t, amountWithdrawn.String(), event.Amount, "Amount") assert.Equal(t, destination.String(), event.Destination, "Destination") - assert.Equal(t, withdrawnBy.String(), event.WithdrawnBy, "WithdrawnBy") + assert.Equal(t, withdrawnBy, event.WithdrawnBy, "WithdrawnBy") assertEverythingSet(t, event, "EventMarketWithdraw") } func TestNewEventMarketDetailsUpdated(t *testing.T) { marketID := uint32(84) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketDetailsUpdated testFunc := func() { event = NewEventMarketDetailsUpdated(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketDetailsUpdated(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketDetailsUpdated(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketDetailsUpdated") } func TestNewEventMarketActiveUpdated(t *testing.T) { - someAddr := sdk.AccAddress("some_address________") + someAddr := sdk.AccAddress("some_address________").String() tests := []struct { name string marketID uint32 - updatedBy sdk.AccAddress + updatedBy string isActive bool expected proto.Message }{ @@ -434,48 +434,48 @@ func TestNewEventMarketActiveUpdated(t *testing.T) { event = NewEventMarketActiveUpdated(tc.marketID, tc.updatedBy, tc.isActive) } require.NotPanics(t, testFunc, "NewEventMarketActiveUpdated(%d, %q, %t) result", - tc.marketID, tc.updatedBy.String(), tc.isActive) + tc.marketID, tc.updatedBy, tc.isActive) assert.Equal(t, tc.expected, event, "NewEventMarketActiveUpdated(%d, %q, %t) result", - tc.marketID, tc.updatedBy.String(), tc.isActive) + tc.marketID, tc.updatedBy, tc.isActive) }) } } func TestNewEventMarketEnabled(t *testing.T) { marketID := uint32(919) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketEnabled testFunc := func() { event = NewEventMarketEnabled(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketEnabled(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketEnabled(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketEnabled") } func TestNewEventMarketDisabled(t *testing.T) { marketID := uint32(5555) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketDisabled testFunc := func() { event = NewEventMarketDisabled(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketDisabled(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketDisabled(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketDisabled") } func TestNewEventMarketUserSettleUpdated(t *testing.T) { - someAddr := sdk.AccAddress("some_address________") + someAddr := sdk.AccAddress("some_address________").String() tests := []struct { name string marketID uint32 - updatedBy sdk.AccAddress + updatedBy string isAllowed bool expected proto.Message }{ @@ -502,66 +502,66 @@ func TestNewEventMarketUserSettleUpdated(t *testing.T) { event = NewEventMarketUserSettleUpdated(tc.marketID, tc.updatedBy, tc.isAllowed) } require.NotPanics(t, testFunc, "NewEventMarketUserSettleUpdated(%d, %q, %t) result", - tc.marketID, tc.updatedBy.String(), tc.isAllowed) + tc.marketID, tc.updatedBy, tc.isAllowed) assert.Equal(t, tc.expected, event, "NewEventMarketUserSettleUpdated(%d, %q, %t) result", - tc.marketID, tc.updatedBy.String(), tc.isAllowed) + tc.marketID, tc.updatedBy, tc.isAllowed) }) } } func TestNewEventMarketUserSettleEnabled(t *testing.T) { marketID := uint32(123) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketUserSettleEnabled testFunc := func() { event = NewEventMarketUserSettleEnabled(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketUserSettleEnabled(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketUserSettleEnabled(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketUserSettleEnabled") } func TestNewEventMarketUserSettleDisabled(t *testing.T) { marketID := uint32(123) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketUserSettleDisabled testFunc := func() { event = NewEventMarketUserSettleDisabled(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketUserSettleDisabled(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketUserSettleDisabled(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketUserSettleDisabled") } func TestNewEventMarketPermissionsUpdated(t *testing.T) { marketID := uint32(5432) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketPermissionsUpdated testFunc := func() { event = NewEventMarketPermissionsUpdated(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketPermissionsUpdated(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketPermissionsUpdated(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketPermissionsUpdated") } func TestNewEventMarketReqAttrUpdated(t *testing.T) { marketID := uint32(3334) - updatedBy := sdk.AccAddress("updatedBy___________") + updatedBy := sdk.AccAddress("updatedBy___________").String() var event *EventMarketReqAttrUpdated testFunc := func() { event = NewEventMarketReqAttrUpdated(marketID, updatedBy) } - require.NotPanics(t, testFunc, "NewEventMarketReqAttrUpdated(%d, %q)", marketID, string(updatedBy)) + require.NotPanics(t, testFunc, "NewEventMarketReqAttrUpdated(%d, %q)", marketID, updatedBy) assert.Equal(t, marketID, event.MarketId, "MarketId") - assert.Equal(t, updatedBy.String(), event.UpdatedBy, "UpdatedBy") + assert.Equal(t, updatedBy, event.UpdatedBy, "UpdatedBy") assertEverythingSet(t, event, "EventMarketReqAttrUpdated") } @@ -602,14 +602,14 @@ func TestTypedEventToEvent(t *testing.T) { quoteBz := func(str string) []byte { return []byte(fmt.Sprintf("%q", str)) } - cancelledBy := sdk.AccAddress("cancelledBy_________") - cancelledByQ := quoteBz(cancelledBy.String()) + cancelledBy := "cancelledBy_________" + cancelledByQ := quoteBz(cancelledBy) destination := sdk.AccAddress("destination_________") destinationQ := quoteBz(destination.String()) withdrawnBy := sdk.AccAddress("withdrawnBy_________") withdrawnByQ := quoteBz(withdrawnBy.String()) - updatedBy := sdk.AccAddress("updatedBy___________") - updatedByQ := quoteBz(updatedBy.String()) + updatedBy := "updatedBy___________" + updatedByQ := quoteBz(updatedBy) coins1 := sdk.NewCoins(sdk.NewInt64Coin("onecoin", 1), sdk.NewInt64Coin("twocoin", 2)) coins1Q := quoteBz(coins1.String()) acoin := sdk.NewInt64Coin("acoin", 55) @@ -786,7 +786,7 @@ func TestTypedEventToEvent(t *testing.T) { }, { name: "EventMarketWithdraw", - tev: NewEventMarketWithdraw(6, coins1, destination, withdrawnBy), + tev: NewEventMarketWithdraw(6, coins1, destination, withdrawnBy.String()), expEvent: sdk.Event{ Type: "provenance.exchange.v1.EventMarketWithdraw", Attributes: []abci.EventAttribute{ diff --git a/x/exchange/keeper/export_test.go b/x/exchange/keeper/export_test.go index f27e9de834..968ac1a293 100644 --- a/x/exchange/keeper/export_test.go +++ b/x/exchange/keeper/export_test.go @@ -33,17 +33,75 @@ func (k Keeper) WithHoldKeeper(holdKeeper exchange.HoldKeeper) Keeper { return k } -// ParseLengthPrefixedAddr is a test-only exposure of parseLengthPrefixedAddr. -var ParseLengthPrefixedAddr = parseLengthPrefixedAddr - // GetStore is a test-only exposure of getStore. func (k Keeper) GetStore(ctx sdk.Context) sdk.KVStore { return k.getStore(ctx) } +// SetOrderInStore is a test-only exposure of setOrderInStore. +func (k Keeper) SetOrderInStore(store sdk.KVStore, order exchange.Order) error { + return k.setOrderInStore(store, order) +} + +// GetOrderStoreKeyValue is a test-only exposure of getOrderStoreKeyValue. +func (k Keeper) GetOrderStoreKeyValue(order exchange.Order) ([]byte, []byte, error) { + return k.getOrderStoreKeyValue(order) +} + var ( // DeleteAll is a test-only exposure of deleteAll. DeleteAll = deleteAll // Iterate is a test-only exposure of iterate. Iterate = iterate + // ParseLengthPrefixedAddr is a test-only exposure of parseLengthPrefixedAddr. + ParseLengthPrefixedAddr = parseLengthPrefixedAddr + // Uint16Bz is a test-only exposure of uint16Bz. + Uint16Bz = uint16Bz + // Uint32Bz is a test-only exposure of uint32Bz. + Uint32Bz = uint32Bz + // Uint64Bz is a test-only exposure of uint64Bz. + Uint64Bz = uint64Bz + + // SetParamsSplit is a test-only exposure of setParamsSplit. + SetParamsSplit = setParamsSplit + + // GetLastAutoMarketID is a test-only exposure of getLastAutoMarketID. + GetLastAutoMarketID = getLastAutoMarketID + // SetLastAutoMarketID is a test-only exposure of setLastAutoMarketID. + SetLastAutoMarketID = setLastAutoMarketID + // SetMarketKnown is a test-only exposure of setMarketKnown. + SetMarketKnown = setMarketKnown + // SetCreateAskFlatFees is a test-only exposure of setCreateAskFlatFees. + SetCreateAskFlatFees = setCreateAskFlatFees + // SetCreateBidFlatFees is a test-only exposure of setCreateBidFlatFees. + SetCreateBidFlatFees = setCreateBidFlatFees + // SetSellerSettlementFlatFees is a test-only exposure of setSellerSettlementFlatFees. + SetSellerSettlementFlatFees = setSellerSettlementFlatFees + // SetBuyerSettlementFlatFees is a test-only exposure of setBuyerSettlementFlatFees. + SetBuyerSettlementFlatFees = setBuyerSettlementFlatFees + // SetSellerSettlementRatios is a test-only exposure of setSellerSettlementRatios. + SetSellerSettlementRatios = setSellerSettlementRatios + // SetBuyerSettlementRatios is a test-only exposure of setBuyerSettlementRatios. + SetBuyerSettlementRatios = setBuyerSettlementRatios + // SetMarketActive is a test-only exposure of setMarketActive. + SetMarketActive = setMarketActive + // SetUserSettlementAllowed is a test-only exposure of setUserSettlementAllowed. + SetUserSettlementAllowed = setUserSettlementAllowed + // GrantPermissions is a test-only exposure of grantPermissions. + GrantPermissions = grantPermissions + // SetReqAttrsAsk is a test-only exposure of setReqAttrsAsk. + SetReqAttrsAsk = setReqAttrsAsk + // SetReqAttrsBid is a test-only exposure of setReqAttrsBid. + SetReqAttrsBid = setReqAttrsBid + // StoreMarket is a test-only exposure of storeMarket. + StoreMarket = storeMarket + + // GetLastOrderID is a test-only exposure of getLastOrderID. + GetLastOrderID = getLastOrderID + // SetLastOrderID is a test-only exposure of setLastOrderID. + SetLastOrderID = setLastOrderID + // CreateConstantIndexEntries is a test-only exposure of createConstantIndexEntries. + CreateConstantIndexEntries = createConstantIndexEntries + // CreateMarketExternalIDToOrderEntry is a test-only exposure of createMarketExternalIDToOrderEntry. + CreateMarketExternalIDToOrderEntry = createMarketExternalIDToOrderEntry ) diff --git a/x/exchange/keeper/fulfillment.go b/x/exchange/keeper/fulfillment.go index 8c8cda994c..867fc05d61 100644 --- a/x/exchange/keeper/fulfillment.go +++ b/x/exchange/keeper/fulfillment.go @@ -80,19 +80,20 @@ func (k Keeper) FillBids(ctx sdk.Context, msg *exchange.MsgFillBidsRequest) erro price := bidOrder.Price buyerFees := bidOrder.BuyerSettlementFees - sellerRatioFee, rerr := calculateSellerSettlementRatioFee(store, marketID, order.GetPrice()) + assetsAddrIdx.Add(buyer, assets) + priceAddrIdx.Add(buyer, price) + feeAddrIdx.Add(buyer, buyerFees...) + settlement.FullyFilledOrders = append(settlement.FullyFilledOrders, exchange.NewFilledOrder(order, price, buyerFees)) + } + + for _, price := range totalPrice { + sellerRatioFee, rerr := calculateSellerSettlementRatioFee(store, marketID, price) if rerr != nil { - errs = append(errs, fmt.Errorf("error calculating seller settlement ratio fee for order %d: %w", - order.OrderId, rerr)) + errs = append(errs, fmt.Errorf("error calculating seller settlement ratio fee: %w", rerr)) } if sellerRatioFee != nil { totalSellerFee = totalSellerFee.Add(*sellerRatioFee) } - - assetsAddrIdx.Add(buyer, assets) - priceAddrIdx.Add(buyer, price) - feeAddrIdx.Add(buyer, buyerFees...) - settlement.FullyFilledOrders = append(settlement.FullyFilledOrders, exchange.NewFilledOrder(order, price, buyerFees)) } if len(errs) > 0 { diff --git a/x/exchange/keeper/fulfillment_test.go b/x/exchange/keeper/fulfillment_test.go index 545847e6f2..f4131775f7 100644 --- a/x/exchange/keeper/fulfillment_test.go +++ b/x/exchange/keeper/fulfillment_test.go @@ -1,7 +1,1870 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestKeeper_FillBids() +import ( + "github.com/gogo/protobuf/proto" -// TODO[1658]: func (s *TestSuite) TestKeeper_FillAsks() + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" -// TODO[1658]: func (s *TestSuite) TestKeeper_SettleOrders() + "github.com/provenance-io/provenance/x/exchange" +) + +func (s *TestSuite) TestKeeper_FillBids() { + tests := []struct { + name string + attrKeeper *MockAttributeKeeper + bankKeeper *MockBankKeeper + holdKeeper *MockHoldKeeper + setup func() + msg exchange.MsgFillBidsRequest + expErr string + expEvents []*exchange.EventOrderFilled + expAttrCalls AttributeCalls + expHoldCalls HoldCalls + expBankCalls BankCalls + }{ + // Tests on error conditions. + { + name: "invalid msg", + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 0, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "invalid market id: cannot be zero", + }, + { + name: "market does not exist", + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "market 1 does not exist", + }, + { + name: "market not accepting orders", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: false, + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "market 1 is not accepting orders", + }, + { + name: "market does not allow user-settle", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, + AllowUserSettlement: false, + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "market 1 does not allow user settlement", + }, + { + name: "seller cannot create ask", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + ReqAttrCreateAsk: []string{"some.attr.no.one.has"}, + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "account " + s.addr1.String() + " is not allowed to create ask orders in market 1", + expAttrCalls: AttributeCalls{GetAllAttributesAddr: [][]byte{s.addr1}}, + }, + { + name: "not enough creation fee", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeCreateAskFlat: s.coins("5fig"), + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + AskOrderCreationFee: s.coinP("4fig"), + }, + expErr: "insufficient ask order creation fee: \"4fig\" is less than required amount \"5fig\"", + }, + { + name: "not enough seller settlement flat fee", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementFlat: s.coins("5fig"), + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + SellerSettlementFlatFee: s.coinP("4fig"), + }, + expErr: "insufficient seller settlement flat fee: \"4fig\" is less than required amount \"5fig\"", + }, + { + name: "bid order does not exist", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{8}, + }, + expErr: "order 8 not found", + }, + { + name: "ask order id provided", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{8}, + }, + expErr: "order 8 is type ask: expected bid", + }, + { + name: "order in wrong market", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{8}, + }, + expErr: "order 8 market id 2 does not equal requested market id 1", + }, + { + name: "order has same buyer as provided seller", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{8}, + }, + expErr: "order 8 has the same buyer " + s.addr1.String() + " as the requested seller", + }, + { + name: "multiple problems with orders", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(17).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(11).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{8, 3, 17, 11}, + }, + expErr: s.joinErrs( + "order 8 not found", + "order 3 market id 2 does not equal requested market id 1", + "order 17 is type ask: expected bid", + "order 11 has the same buyer "+s.addr1.String()+" as the requested seller", + ), + }, + { + name: "provided total assets less than actual total assets", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("1plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("2apple"), Price: s.coin("2plum"), MarketId: 2, Buyer: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + Assets: s.coin("3apple"), Price: s.coin("3plum"), MarketId: 2, Buyer: s.addr3.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("5apple"), + BidOrderIds: []uint64{1, 2, 3}, + }, + expErr: "total assets \"5apple\" does not equal sum of bid order assets \"6apple\"", + }, + { + name: "provided total assets more than actual total assets", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("1plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + Assets: s.coin("2apple"), Price: s.coin("2plum"), MarketId: 2, Buyer: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + Assets: s.coin("3apple"), Price: s.coin("3plum"), MarketId: 2, Buyer: s.addr3.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("7apple"), + BidOrderIds: []uint64{1, 2, 3}, + }, + expErr: "total assets \"7apple\" does not equal sum of bid order assets \"6apple\"", + }, + { + name: "ratio fee calc error", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("20prune:1prune"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("6apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "error calculating seller settlement ratio fee: no seller " + + "settlement fee ratio found for denom \"plum\"", + }, + { + name: "invalid bid order owner", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + key, value, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: "badbuyer", + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 1") + s.getStore().Set(key, value) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("6apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "invalid bid order 1 owner \"badbuyer\": decoding bech32 failed: invalid separator index -1", + }, + { + name: "error releasing hold", + holdKeeper: NewMockHoldKeeper().WithReleaseHoldResults("no plum for you"), + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("6apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "error releasing hold for bid order 1: no plum for you", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("6plum")}}}, + }, + { + name: "error transferring assets", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("first transfer error"), + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "first transfer error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("6plum")}}}, + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("6plum")}, + }}, + }, + { + name: "error transferring price", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("", "second transfer error"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expErr: "second transfer error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("6plum")}}}, + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("6plum")}, + }}, + }, + { + name: "error collecting settlement fees", + bankKeeper: NewMockBankKeeper().WithInputOutputCoinsResults("first fake error"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("6plum:1plum"), + FeeBuyerSettlementRatios: s.ratios("6plum:2fig"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(99).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + BuyerSettlementFees: s.coins("2fig"), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{99}, + }, + expErr: "error collecting fees for market 2: first fake error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("2fig,6plum")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("6plum")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: false, + inputs: []banktypes.Input{ + {Address: s.addr1.String(), Coins: s.coins("2fig")}, + {Address: s.addr4.String(), Coins: s.coins("1plum")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr2.String(), Coins: s.coins("2fig,1plum")}}, + }, + }, + }, + }, + { + name: "error collecting creation fee", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("", "", "another error for testing"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(99).WithBid(&exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Buyer: s.addr1.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{99}, + AskOrderCreationFee: s.coinP("2fig"), + }, + expErr: "error collecting create-ask fee \"2fig\": error transferring 2fig from " + s.addr4.String() + + " to market 2: another error for testing", + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 99, Assets: "1apple", Price: "6plum", MarketId: 2}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("6plum")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("6plum")}, + {ctxHasQuarantineBypass: false, fromAddr: s.addr4, toAddr: s.marketAddr2, amt: s.coins("2fig")}, + }, + }, + }, + + // Tests on successes. + { + name: "one order: no fees", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 6, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(13).WithBid(&exchange.BidOrder{ + Assets: s.coin("12apple"), Price: s.coin("60plum"), MarketId: 6, Buyer: s.addr2.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr5.String(), + MarketId: 6, + TotalAssets: s.coins("12apple"), + BidOrderIds: []uint64{13}, + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("60plum")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr2, amt: s.coins("12apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr5, amt: s.coins("60plum")}, + }, + }, + }, + { + name: "one order: all the fees", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 1000, + DenomSplits: []exchange.DenomSplit{{Denom: "fig", Split: 2000}}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("30plum:1plum"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(13).WithBid(&exchange.BidOrder{ + Assets: s.coin("12apple"), Price: s.coin("60plum"), MarketId: 3, Buyer: s.addr2.String(), + BuyerSettlementFees: s.coins("10fig"), + ExternalId: "thirteen", + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr5.String(), + MarketId: 3, + TotalAssets: s.coins("12apple"), + BidOrderIds: []uint64{13}, + SellerSettlementFlatFee: s.coinP("8plum"), + AskOrderCreationFee: s.coinP("15fig"), + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 13, Assets: "12apple", Price: "60plum", Fees: "10fig", MarketId: 3, ExternalId: "thirteen"}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("10fig,60plum")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr2, amt: s.coins("12apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr5, amt: s.coins("60plum")}, + {ctxHasQuarantineBypass: false, fromAddr: s.addr5, toAddr: s.marketAddr3, amt: s.coins("15fig")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("10fig")}, + {Address: s.addr5.String(), Coins: s.coins("10plum")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr3.String(), Coins: s.coins("10fig,10plum")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("2fig,1plum")}, + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("3fig")}, + }, + }, + }, + { + name: "three orders", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 1000}) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("30plum:1plum,88prune:5prune"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(17).WithBid(&exchange.BidOrder{ + Assets: s.coin("12apple"), Price: s.coin("60plum"), MarketId: 3, Buyer: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + Assets: s.coin("5acorn"), Price: s.coin("50prune"), MarketId: 3, Buyer: s.addr2.String(), + BuyerSettlementFees: s.coins("22fig"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(121).WithBid(&exchange.BidOrder{ + Assets: s.coin("6apple"), Price: s.coin("33prune"), MarketId: 3, Buyer: s.addr3.String(), + })) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 3, + TotalAssets: s.coins("5acorn,18apple"), + BidOrderIds: []uint64{55, 121, 17}, + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 55, Assets: "5acorn", Price: "50prune", MarketId: 3, Fees: "22fig"}, + {OrderId: 121, Assets: "6apple", Price: "33prune", MarketId: 3}, + {OrderId: 17, Assets: "12apple", Price: "60plum", MarketId: 3}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr2, funds: s.coins("22fig,50prune")}, + {addr: s.addr3, funds: s.coins("33prune")}, + {addr: s.addr2, funds: s.coins("60plum")}, + }}, + expBankCalls: BankCalls{ + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{{Address: s.addr1.String(), Coins: s.coins("5acorn,18apple")}}, + outputs: []banktypes.Output{ + {Address: s.addr2.String(), Coins: s.coins("5acorn,12apple")}, + {Address: s.addr3.String(), Coins: s.coins("6apple")}, + }, + }, + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("60plum,50prune")}, + {Address: s.addr3.String(), Coins: s.coins("33prune")}, + }, + outputs: []banktypes.Output{{Address: s.addr1.String(), Coins: s.coins("60plum,83prune")}}, + }, + { + ctxHasQuarantineBypass: false, + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("22fig")}, + {Address: s.addr1.String(), Coins: s.coins("2plum,5prune")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr3.String(), Coins: s.coins("22fig,2plum,5prune")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("3fig,1plum,1prune")}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + if s.accKeeper == nil { + s.accKeeper = NewMockAccountKeeper() + } + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + + expEvents := untypeEvents(s, tc.expEvents) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + kpr := s.k.WithAttributeKeeper(tc.attrKeeper). + WithAccountKeeper(s.accKeeper). + WithBankKeeper(tc.bankKeeper). + WithHoldKeeper(tc.holdKeeper) + var err error + testFunc := func() { + err = kpr.FillBids(ctx, &tc.msg) + } + s.Require().NotPanics(testFunc, "FillBids") + s.assertErrorValue(err, tc.expErr, "FillBids error") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "FillBids events") + s.assertAttributeKeeperCalls(tc.attrKeeper, tc.expAttrCalls, "FillBids") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "FillBids") + s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "FillBids") + + if len(actEvents) == 0 { + return + } + + // Make sure all the orders have been deleted. + for _, orderID := range tc.msg.BidOrderIds { + order, oerr := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(oerr, "GetOrder(%d) after FillBids", orderID) + s.Assert().Nil(order, "GetOrder(%d) after FillBids", orderID) + } + }) + } +} + +func (s *TestSuite) TestKeeper_FillAsks() { + tests := []struct { + name string + attrKeeper *MockAttributeKeeper + bankKeeper *MockBankKeeper + holdKeeper *MockHoldKeeper + setup func() + msg exchange.MsgFillAsksRequest + expErr string + expEvents []*exchange.EventOrderFilled + expAttrCalls AttributeCalls + expHoldCalls HoldCalls + expBankCalls BankCalls + }{ + // Tests on error conditions. + { + name: "invalid msg", + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 0, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + }, + expErr: "invalid market id: cannot be zero", + }, + { + name: "market does not exist", + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + }, + expErr: "market 1 does not exist", + }, + { + name: "market not accepting orders", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: false, + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + }, + expErr: "market 1 is not accepting orders", + }, + { + name: "market does not allow user-settle", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, + AllowUserSettlement: false, + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + }, + expErr: "market 1 does not allow user settlement", + }, + { + name: "buyer cannot create bid", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + ReqAttrCreateBid: []string{"some.attr.no.one.has"}, + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + }, + expErr: "account " + s.addr1.String() + " is not allowed to create bid orders in market 1", + expAttrCalls: AttributeCalls{GetAllAttributesAddr: [][]byte{s.addr1}}, + }, + { + name: "not enough creation fee", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeCreateBidFlat: s.coins("5fig"), + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + BidOrderCreationFee: s.coinP("4fig"), + }, + expErr: "insufficient bid order creation fee: \"4fig\" is less than required amount \"5fig\"", + }, + { + name: "not enough buyer settlement fee", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeBuyerSettlementFlat: s.coins("5fig"), + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{1}, + BuyerSettlementFees: s.coins("4fig"), + }, + expErr: s.joinErrs( + "4fig is less than required flat fee 5fig", + "required flat fee not satisfied, valid options: 5fig", + "insufficient buyer settlement fee 4fig", + ), + }, + { + name: "ask order does not exist", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1prune"), + AskOrderIds: []uint64{8}, + }, + expErr: "order 8 not found", + }, + { + name: "bid order id provided", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1plum"), + AskOrderIds: []uint64{8}, + }, + expErr: "order 8 is type bid: expected ask", + }, + { + name: "order in wrong market", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithAsk(&exchange.AskOrder{ + MarketId: 2, + Seller: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1plum"), + AskOrderIds: []uint64{8}, + }, + expErr: "order 8 market id 2 does not equal requested market id 1", + }, + { + name: "order has same seller as provided buyer", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1plum"), + AskOrderIds: []uint64{8}, + }, + expErr: "order 8 has the same seller " + s.addr1.String() + " as the requested buyer", + }, + { + name: "multiple problems with orders", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 2, + Seller: s.addr2.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(17).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(11).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1plum"), + AskOrderIds: []uint64{8, 3, 17, 11}, + }, + expErr: s.joinErrs( + "order 8 not found", + "order 3 market id 2 does not equal requested market id 1", + "order 17 is type bid: expected ask", + "order 11 has the same seller "+s.addr1.String()+" as the requested buyer", + ), + }, + { + name: "provided total price less than actual total price", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("1plum"), MarketId: 2, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(2).WithAsk(&exchange.AskOrder{ + Assets: s.coin("2apple"), Price: s.coin("2plum"), MarketId: 2, Seller: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("3apple"), Price: s.coin("3plum"), MarketId: 2, Seller: s.addr3.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("5plum"), + AskOrderIds: []uint64{1, 2, 3}, + }, + expErr: "total price \"5plum\" does not equal sum of ask order prices \"6plum\"", + }, + { + name: "provided total price more than actual total price", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("1plum"), MarketId: 2, Seller: s.addr1.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(2).WithAsk(&exchange.AskOrder{ + Assets: s.coin("2apple"), Price: s.coin("2plum"), MarketId: 2, Seller: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + Assets: s.coin("3apple"), Price: s.coin("3plum"), MarketId: 2, Seller: s.addr3.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("7plum"), + AskOrderIds: []uint64{1, 2, 3}, + }, + expErr: "total price \"7plum\" does not equal sum of ask order prices \"6plum\"", + }, + { + name: "ratio fee calc error", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("20prune:1prune"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{1}, + }, + expErr: "error calculating seller settlement ratio fee for order 1: no seller " + + "settlement fee ratio found for denom \"plum\"", + }, + { + name: "invalid bid order owner", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + key, value, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Seller: "badseller", + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 1") + s.getStore().Set(key, value) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{1}, + }, + expErr: "invalid ask order 1 owner \"badseller\": decoding bech32 failed: invalid separator index -1", + }, + { + name: "error releasing hold", + holdKeeper: NewMockHoldKeeper().WithReleaseHoldResults("no apple for you"), + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("6apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{1}, + }, + expErr: "error releasing hold for ask order 1: no apple for you", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("6apple")}}}, + }, + { + name: "error transferring assets", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("first transfer error"), + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{1}, + }, + expErr: "first transfer error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("1apple")}}}, + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("6plum")}, + }}, + }, + { + name: "error transferring price", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("", "second transfer error"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{1}, + }, + expErr: "second transfer error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("1apple")}}}, + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("6plum")}, + }}, + }, + { + name: "error collecting settlement fees", + bankKeeper: NewMockBankKeeper().WithInputOutputCoinsResults("first fake error"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("6plum:1plum"), + FeeBuyerSettlementRatios: s.ratios("6plum:2fig"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + SellerSettlementFlatFee: s.coinP("2fig"), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{99}, + BuyerSettlementFees: s.coins("2fig"), + }, + expErr: "error collecting fees for market 2: first fake error", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("2fig,1apple")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("6plum")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: false, + inputs: []banktypes.Input{ + {Address: s.addr1.String(), Coins: s.coins("2fig,1plum")}, + {Address: s.addr4.String(), Coins: s.coins("2fig")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr2.String(), Coins: s.coins("4fig,1plum")}}, + }, + }, + }, + }, + { + name: "error collecting creation fee", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("", "", "another error for testing"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{}) + s.requireCreateMarket(exchange.Market{MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("6plum"), MarketId: 2, Seller: s.addr1.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr4.String(), + MarketId: 2, + TotalPrice: s.coin("6plum"), + AskOrderIds: []uint64{99}, + BidOrderCreationFee: s.coinP("2fig"), + }, + expErr: "error collecting create-ask fee \"2fig\": error transferring 2fig from " + s.addr4.String() + + " to market 2: another error for testing", + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 99, Assets: "1apple", Price: "6plum", MarketId: 2}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("1apple")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr1, toAddr: s.addr4, amt: s.coins("1apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr1, amt: s.coins("6plum")}, + {ctxHasQuarantineBypass: false, fromAddr: s.addr4, toAddr: s.marketAddr2, amt: s.coins("2fig")}, + }, + }, + }, + + // Tests on successes. + { + name: "one order: no fees", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 6, AcceptingOrders: true, AllowUserSettlement: true}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(13).WithAsk(&exchange.AskOrder{ + Assets: s.coin("12apple"), Price: s.coin("60plum"), MarketId: 6, Seller: s.addr2.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr5.String(), + MarketId: 6, + TotalPrice: s.coin("60plum"), + AskOrderIds: []uint64{13}, + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("12apple")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr5, amt: s.coins("12apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr2, amt: s.coins("60plum")}, + }, + }, + }, + { + name: "one order: all the fees", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 1000, + DenomSplits: []exchange.DenomSplit{{Denom: "fig", Split: 2000}}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("30plum:1plum"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(13).WithAsk(&exchange.AskOrder{ + Assets: s.coin("12apple"), Price: s.coin("60plum"), MarketId: 3, Seller: s.addr2.String(), + SellerSettlementFlatFee: s.coinP("8fig"), + ExternalId: "thirteen", + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr5.String(), + MarketId: 3, + TotalPrice: s.coin("60plum"), + AskOrderIds: []uint64{13}, + BuyerSettlementFees: s.coins("10plum"), + BidOrderCreationFee: s.coinP("15fig"), + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 13, Assets: "12apple", Price: "60plum", Fees: "8fig,2plum", MarketId: 3, ExternalId: "thirteen"}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("12apple,8fig")}}}, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr2, toAddr: s.addr5, amt: s.coins("12apple")}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr5, toAddr: s.addr2, amt: s.coins("60plum")}, + {ctxHasQuarantineBypass: false, fromAddr: s.addr5, toAddr: s.marketAddr3, amt: s.coins("15fig")}, + }, + InputOutputCoins: []*InputOutputCoinsArgs{ + { + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("8fig,2plum")}, + {Address: s.addr5.String(), Coins: s.coins("10plum")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr3.String(), Coins: s.coins("8fig,12plum")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("2fig,2plum")}, + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("3fig")}, + }, + }, + }, + { + name: "three orders", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 1000}) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + FeeSellerSettlementRatios: s.ratios("143prune:5prune"), + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(17).WithAsk(&exchange.AskOrder{ + Assets: s.coin("12apple"), Price: s.coin("60prune"), MarketId: 3, Seller: s.addr2.String(), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(55).WithAsk(&exchange.AskOrder{ + Assets: s.coin("5acorn"), Price: s.coin("50prune"), MarketId: 3, Seller: s.addr2.String(), + SellerSettlementFlatFee: s.coinP("22fig"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(121).WithAsk(&exchange.AskOrder{ + Assets: s.coin("6apple"), Price: s.coin("33prune"), MarketId: 3, Seller: s.addr3.String(), + })) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 3, + TotalPrice: s.coin("143prune"), + AskOrderIds: []uint64{55, 121, 17}, + }, + expEvents: []*exchange.EventOrderFilled{ + {OrderId: 55, Assets: "5acorn", Price: "50prune", MarketId: 3, Fees: "22fig,2prune"}, + {OrderId: 121, Assets: "6apple", Price: "33prune", MarketId: 3, Fees: "2prune"}, + {OrderId: 17, Assets: "12apple", Price: "60prune", MarketId: 3, Fees: "3prune"}, + }, + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr2, funds: s.coins("5acorn,22fig")}, + {addr: s.addr3, funds: s.coins("6apple")}, + {addr: s.addr2, funds: s.coins("12apple")}, + }}, + expBankCalls: BankCalls{ + InputOutputCoins: []*InputOutputCoinsArgs{ + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("5acorn,12apple")}, + {Address: s.addr3.String(), Coins: s.coins("6apple")}, + }, + outputs: []banktypes.Output{{Address: s.addr1.String(), Coins: s.coins("5acorn,18apple")}}, + }, + { + ctxHasQuarantineBypass: true, + inputs: []banktypes.Input{ + {Address: s.addr1.String(), Coins: s.coins("143prune")}, + }, + outputs: []banktypes.Output{ + {Address: s.addr2.String(), Coins: s.coins("110prune")}, + {Address: s.addr3.String(), Coins: s.coins("33prune")}, + }, + }, + { + ctxHasQuarantineBypass: false, + inputs: []banktypes.Input{ + {Address: s.addr2.String(), Coins: s.coins("22fig,5prune")}, + {Address: s.addr3.String(), Coins: s.coins("2prune")}, + }, + outputs: []banktypes.Output{{Address: s.marketAddr3.String(), Coins: s.coins("22fig,7prune")}}, + }, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("3fig,1prune")}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + if s.accKeeper == nil { + s.accKeeper = NewMockAccountKeeper() + } + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + + expEvents := untypeEvents(s, tc.expEvents) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + kpr := s.k.WithAttributeKeeper(tc.attrKeeper). + WithAccountKeeper(s.accKeeper). + WithBankKeeper(tc.bankKeeper). + WithHoldKeeper(tc.holdKeeper) + var err error + testFunc := func() { + err = kpr.FillAsks(ctx, &tc.msg) + } + s.Require().NotPanics(testFunc, "FillAsks") + s.assertErrorValue(err, tc.expErr, "FillAsks error") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "FillAsks events") + s.assertAttributeKeeperCalls(tc.attrKeeper, tc.expAttrCalls, "FillAsks") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "FillAsks") + s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "FillAsks") + + if len(actEvents) == 0 { + return + } + + // Make sure all the orders have been deleted. + for _, orderID := range tc.msg.AskOrderIds { + order, oerr := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(oerr, "GetOrder(%d) after FillAsks", orderID) + s.Assert().Nil(order, "GetOrder(%d) after FillAsks", orderID) + } + }) + } +} + +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 := untypeEvents(s, tc.expEvents) + + 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) + } + }) + } +} diff --git a/x/exchange/keeper/genesis.go b/x/exchange/keeper/genesis.go index afcff92f8a..45425a3a56 100644 --- a/x/exchange/keeper/genesis.go +++ b/x/exchange/keeper/genesis.go @@ -26,6 +26,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *exchange.GenesisState) { var addrs []string amounts := make(map[string]sdk.Coins) + var maxOrderID uint64 for i, order := range genState.Orders { if err := k.setOrderInStore(store, order); err != nil { panic(fmt.Errorf("failed to store Orders[%d]: %w", i, err)) @@ -36,8 +37,14 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *exchange.GenesisState) { amounts[addr] = nil } amounts[addr] = amounts[addr].Add(order.GetHoldAmount()...) + if order.OrderId > maxOrderID { + maxOrderID = order.OrderId + } } + if genState.LastOrderId < maxOrderID { + panic(fmt.Errorf("last order id %d is less than largest order id %d", genState.LastOrderId, maxOrderID)) + } setLastOrderID(store, genState.LastOrderId) // Make sure all the needed funds have holds on them. These should have been placed during initialization of the hold module. @@ -45,7 +52,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *exchange.GenesisState) { for _, reqAmt := range amounts[addr] { holdAmt, err := k.holdKeeper.GetHoldCoin(ctx, sdk.MustAccAddressFromBech32(addr), reqAmt.Denom) if err != nil { - panic(fmt.Errorf("failed to look up amount of %q on hold for %s", reqAmt.Denom, addr)) + panic(fmt.Errorf("failed to look up amount of %q on hold for %s: %w", reqAmt.Denom, addr, err)) } if holdAmt.Amount.LT(reqAmt.Amount) { panic(fmt.Errorf("account %s should have at least %q on hold (due to exchange orders), but only has %q", addr, reqAmt, holdAmt)) diff --git a/x/exchange/keeper/genesis_test.go b/x/exchange/keeper/genesis_test.go index e822f9416d..4fda0b0dbf 100644 --- a/x/exchange/keeper/genesis_test.go +++ b/x/exchange/keeper/genesis_test.go @@ -1,5 +1,501 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestKeeper_InitGenesis() +import ( + "fmt" -// TODO[1658]: func (s *TestSuite) TestKeeper_ExportGenesis() + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) + +func (s *TestSuite) TestKeeper_InitAndExportGenesis() { + marketAcc := func(marketID uint32, name string) *exchange.MarketAccount { + return &exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{Address: exchange.GetMarketAddress(marketID).String()}, + MarketId: marketID, + MarketDetails: exchange.MarketDetails{Name: name}, + } + } + accAddr := func(prefix string, orderID uint64) sdk.AccAddress { + return sdk.AccAddress(fmt.Sprintf("%s%d____________________", prefix, orderID)[:20]) + } + assetDenom, priceDenom, feeDenom := "apple", "pear", "fig" + askOrder := func(orderID uint64, marketID uint32, seller string) exchange.Order { + if len(seller) == 0 { + seller = accAddr("seller", orderID).String() + } + return *exchange.NewOrder(orderID).WithAsk(&exchange.AskOrder{ + MarketId: marketID, + Seller: seller, + Assets: s.coin(fmt.Sprintf("%d%s", orderID, assetDenom)), + Price: s.coin(fmt.Sprintf("%d%s", orderID, priceDenom)), + SellerSettlementFlatFee: s.coinP(fmt.Sprintf("%d%s", orderID, feeDenom)), + AllowPartial: true, + ExternalId: fmt.Sprintf("ExtId%d", orderID), + }) + } + bidOrder := func(orderID uint64, marketID uint32, buyer string) exchange.Order { + if len(buyer) == 0 { + buyer = accAddr("buyer", orderID).String() + } + return *exchange.NewOrder(orderID).WithBid(&exchange.BidOrder{ + MarketId: marketID, + Buyer: buyer, + Assets: s.coin(fmt.Sprintf("%d%s", orderID, assetDenom)), + Price: s.coin(fmt.Sprintf("%d%s", orderID, priceDenom)), + BuyerSettlementFees: s.coins(fmt.Sprintf("%d%s", orderID, feeDenom)), + AllowPartial: true, + ExternalId: fmt.Sprintf("ExtId%d", orderID), + }) + } + askHoldCoins := func(orderID uint64) sdk.Coins { + return s.coins(fmt.Sprintf("%d%s,%d%s", orderID, assetDenom, orderID, feeDenom)) + } + bidHoldCoins := func(orderID uint64) sdk.Coins { + return s.coins(fmt.Sprintf("%d%s,%d%s", orderID, priceDenom, orderID, feeDenom)) + } + + tests := []struct { + name string + accKeeper *MockAccountKeeper + holdKeeper *MockHoldKeeper + setup func() + genState *exchange.GenesisState + expGenState *exchange.GenesisState + expInitPanic string + expExportLog string + expAccCalls AccountCalls + expHoldCalls HoldCalls + }{ + { + name: "nil gen state", + genState: nil, + }, + { + name: "empty gen state", + genState: &exchange.GenesisState{}, + }, + { + name: "just params: no splits", + genState: &exchange.GenesisState{ + Params: &exchange.Params{ + DefaultSplit: 777, + DenomSplits: nil, + }, + }, + }, + { + name: "just params: one split", + genState: &exchange.GenesisState{ + Params: &exchange.Params{ + DefaultSplit: 777, + DenomSplits: []exchange.DenomSplit{ + {Denom: "yam", Split: 333}, + }, + }, + }, + }, + { + name: "just params: three splits", + genState: &exchange.GenesisState{ + Params: &exchange.Params{ + DefaultSplit: 777, + DenomSplits: []exchange.DenomSplit{ + {Denom: "green", Split: 999}, + {Denom: "orange", Split: 100}, + {Denom: "yellow", Split: 543}, + }, + }, + }, + }, + { + name: "one market: account already exists with same details", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(s.marketAddr1, marketAcc(1, "some name")), + genState: &exchange.GenesisState{ + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "some name"}, + FeeCreateAskFlat: s.coins("1apple"), + FeeCreateBidFlat: s.coins("2banana"), + FeeSellerSettlementFlat: s.coins("3cactus"), + FeeSellerSettlementRatios: s.ratios("4damson:5elderberry"), + FeeBuyerSettlementFlat: s.coins("6fig"), + FeeBuyerSettlementRatios: s.ratios("7grape:8honeydew"), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + }, + ReqAttrCreateAsk: []string{"ask.create.req"}, + ReqAttrCreateBid: []string{"bid.create.req"}, + }, + }, + }, + expAccCalls: AccountCalls{GetAccount: []sdk.AccAddress{s.marketAddr1}}, + }, + { + name: "one market: account already exists with different details", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(s.marketAddr2, marketAcc(2, "existing name")), + genState: &exchange.GenesisState{ + Markets: []exchange.Market{ + { + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "new name"}, + FeeCreateAskFlat: s.coins("1apple"), + FeeSellerSettlementFlat: s.coins("3cactus"), + FeeSellerSettlementRatios: s.ratios("4damson:5elderberry"), + ReqAttrCreateAsk: []string{"ask.create.req"}, + }, + }, + }, + expAccCalls: AccountCalls{ + GetAccount: []sdk.AccAddress{s.marketAddr2}, + SetAccount: []authtypes.AccountI{marketAcc(2, "new name")}, + }, + }, + { + name: "one market: account does not yet exist", + genState: &exchange.GenesisState{ + Markets: []exchange.Market{{MarketId: 3, MarketDetails: exchange.MarketDetails{Name: "Name Three"}}}, + }, + expAccCalls: AccountCalls{ + GetAccount: []sdk.AccAddress{s.marketAddr3}, + NewAccount: []authtypes.AccountI{marketAcc(3, "Name Three")}, + SetAccount: []authtypes.AccountI{marketAcc(3, "Name Three")}, + }, + }, + { + name: "three markets", + // First will not yet have an account + // Second will have an account with different details + // Third will have an account with the same details + accKeeper: NewMockAccountKeeper(). + WithGetAccountResult(exchange.GetMarketAddress(75), marketAcc(75, "Original Second")). + WithGetAccountResult(s.marketAddr3, marketAcc(3, "Third")), + genState: &exchange.GenesisState{ + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "First"}, + FeeCreateAskFlat: s.coins("1apple"), + FeeSellerSettlementFlat: s.coins("3cactus"), + FeeSellerSettlementRatios: s.ratios("4damson:5elderberry"), + AcceptingOrders: true, + ReqAttrCreateAsk: []string{"ask.create.req"}, + }, + { + MarketId: 75, + MarketDetails: exchange.MarketDetails{Name: "New Second Wave"}, + FeeCreateBidFlat: s.coins("2banana"), + FeeBuyerSettlementFlat: s.coins("6fig"), + FeeBuyerSettlementRatios: s.ratios("7grape:8honeydew"), + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: []exchange.Permission{1}}, + {Address: s.addr2.String(), Permissions: exchange.AllPermissions()}, + }, + ReqAttrCreateBid: []string{"bid.create.req"}, + }, + { + MarketId: 3, + MarketDetails: exchange.MarketDetails{Name: "Third"}, + FeeSellerSettlementRatios: nil, + FeeBuyerSettlementRatios: nil, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: []exchange.Permission{1, 2}}, + {Address: s.addr2.String(), Permissions: []exchange.Permission{3, 4}}, + {Address: s.addr3.String(), Permissions: []exchange.Permission{5, 6}}, + {Address: s.addr4.String(), Permissions: exchange.AllPermissions()}, + {Address: s.addr5.String(), Permissions: []exchange.Permission{7, 8}}, + }, + }, + }, + }, + expAccCalls: AccountCalls{ + GetAccount: []sdk.AccAddress{s.marketAddr1, exchange.GetMarketAddress(75), s.marketAddr3}, + NewAccount: []authtypes.AccountI{marketAcc(1, "First")}, + SetAccount: []authtypes.AccountI{marketAcc(1, "First"), marketAcc(75, "New Second Wave")}, + }, + }, + { + name: "one order: ask", + holdKeeper: NewMockHoldKeeper().WithGetHoldCoinResult(accAddr("seller", 7), askHoldCoins(7)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{askOrder(7, 2, "")}, + LastOrderId: 7, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{ + {addr: accAddr("seller", 7), denom: assetDenom}, + {addr: accAddr("seller", 7), denom: feeDenom}, + }, + }, + }, + { + name: "one order: bid", + holdKeeper: NewMockHoldKeeper().WithGetHoldCoinResult(accAddr("buyer", 4), bidHoldCoins(4)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{bidOrder(4, 1, "")}, + LastOrderId: 4, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{ + {addr: accAddr("buyer", 4), denom: feeDenom}, + {addr: accAddr("buyer", 4), denom: priceDenom}, + }, + }, + }, + { + name: "several orders", + holdKeeper: NewMockHoldKeeper(). + WithGetHoldCoinResult(accAddr("buyer", 70), bidHoldCoins(100)...). // extra should be okay. + WithGetHoldCoinResult(accAddr("seller", 55), askHoldCoins(55)...). + WithGetHoldCoinResult(s.addr1, bidHoldCoins(2).Add(askHoldCoins(44)...)...). + WithGetHoldCoinResult(accAddr("buyer", 25), bidHoldCoins(25)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{ + bidOrder(70, 95, ""), + askOrder(55, 8, ""), + bidOrder(2, 8, s.addr1.String()), + bidOrder(25, 36, ""), + askOrder(33, 95, s.addr1.String()), + askOrder(11, 95, s.addr1.String()), + }, + LastOrderId: 100, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{ + {addr: accAddr("buyer", 70), denom: feeDenom}, {addr: accAddr("buyer", 70), denom: priceDenom}, + {addr: accAddr("seller", 55), denom: assetDenom}, {addr: accAddr("seller", 55), denom: feeDenom}, + {addr: s.addr1, denom: assetDenom}, {addr: s.addr1, denom: feeDenom}, {addr: s.addr1, denom: priceDenom}, + {addr: accAddr("buyer", 25), denom: feeDenom}, {addr: accAddr("buyer", 25), denom: priceDenom}, + }, + }, + }, + { + name: "error setting order", + genState: &exchange.GenesisState{ + Orders: []exchange.Order{ + *exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: accAddr("seller", 1).String(), + Assets: s.coin("1" + assetDenom), + Price: s.coin("1" + priceDenom), + ExternalId: "duplicate external id", + }), + *exchange.NewOrder(2).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: accAddr("seller", 2).String(), + Assets: s.coin("2" + assetDenom), + Price: s.coin("2" + priceDenom), + ExternalId: "duplicate external id", + }), + }, + }, + expInitPanic: "failed to store Orders[1]: external id \"duplicate external id\" is already " + + "in use by order 1: cannot be used for order 2", + }, + { + name: "error checking holds", + holdKeeper: NewMockHoldKeeper().WithGetHoldCoinErrorResult(accAddr("buyer", 1), feeDenom, + "this is an error that has been injected for testing"), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{bidOrder(1, 1, "")}, + LastOrderId: 1, + }, + expInitPanic: "failed to look up amount of \"" + feeDenom + "\" on hold for " + + accAddr("buyer", 1).String() + ": this is an error that has been injected for testing", + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{{addr: accAddr("buyer", 1), denom: feeDenom}}, + }, + }, + { + name: "not enough hold on account: ask", + holdKeeper: NewMockHoldKeeper().WithGetHoldCoinResult(accAddr("seller", 7), askHoldCoins(6)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{askOrder(7, 2, "")}, + LastOrderId: 7, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{{addr: accAddr("seller", 7), denom: assetDenom}}, + }, + expInitPanic: "account " + accAddr("seller", 7).String() + " should have at least \"7" + assetDenom + "\" on hold " + + "(due to exchange orders), but only has \"6" + assetDenom + "\"", + }, + { + name: "not enough hold on account: bid", + holdKeeper: NewMockHoldKeeper().WithGetHoldCoinResult(accAddr("buyer", 777), bidHoldCoins(776)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{bidOrder(777, 1, "")}, + LastOrderId: 1000, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{{addr: accAddr("buyer", 777), denom: feeDenom}}, + }, + expInitPanic: "account " + accAddr("buyer", 777).String() + " should have at least \"777" + feeDenom + "\" on hold " + + "(due to exchange orders), but only has \"776" + feeDenom + "\"", + }, + { + name: "last order id too low", + holdKeeper: NewMockHoldKeeper(). + WithGetHoldCoinResult(accAddr("buyer", 70), bidHoldCoins(100)...). // extra should be okay. + WithGetHoldCoinResult(accAddr("seller", 55), askHoldCoins(55)...). + WithGetHoldCoinResult(s.addr1, bidHoldCoins(2).Add(askHoldCoins(44)...)...). + WithGetHoldCoinResult(accAddr("buyer", 25), bidHoldCoins(25)...), + genState: &exchange.GenesisState{ + Orders: []exchange.Order{ + bidOrder(70, 95, ""), + askOrder(55, 8, ""), + bidOrder(2, 8, s.addr1.String()), + bidOrder(25, 36, ""), + askOrder(33, 95, s.addr1.String()), + askOrder(11, 95, s.addr1.String()), + }, + LastOrderId: 69, + }, + expInitPanic: "last order id 69 is less than largest order id 70", + }, + { + name: "just last market id", + genState: &exchange.GenesisState{LastMarketId: 8}, + }, + { + name: "just last order id", + genState: &exchange.GenesisState{LastOrderId: 9}, + }, + { + name: "error reading orders", + setup: func() { + store := s.getStore() + order1 := askOrder(1, 1, "") + s.requireSetOrderInStore(store, &order1) + key2, value2, err := s.k.GetOrderStoreKeyValue(askOrder(2, 1, "")) + s.Require().NoError(err, "GetOrderStoreKeyValue 2") + value2[0] = 8 + store.Set(key2, value2) + key3, value3, err := s.k.GetOrderStoreKeyValue(bidOrder(3, 1, "")) + s.Require().NoError(err, "GetOrderStoreKeyValue 3") + value3[0] = 8 + store.Set(key3, value3) + order4 := bidOrder(4, 1, "") + s.requireSetOrderInStore(store, &order4) + keeper.SetLastOrderID(store, 4) + }, + expGenState: &exchange.GenesisState{ + Orders: []exchange.Order{askOrder(1, 1, ""), bidOrder(4, 1, "")}, + LastOrderId: 4, + }, + expExportLog: "ERR error (ignored) while reading orders: failed to read order 2: unknown type byte 0x8\n" + + "failed to read order 3: unknown type byte 0x8 module=x/exchange\n", + }, + { + name: "a little of everything", + holdKeeper: NewMockHoldKeeper(). + WithGetHoldCoinResult(s.addr1, askHoldCoins(1)...). + WithGetHoldCoinResult(s.addr2, bidHoldCoins(10)...). + WithGetHoldCoinResult(s.addr3, bidHoldCoins(77).Add(askHoldCoins(79)...)...). + WithGetHoldCoinResult(s.addr4, askHoldCoins(1101)...), + genState: &exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 333}, + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "First Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + }, + }, + { + MarketId: 420, + MarketDetails: exchange.MarketDetails{Name: "THE Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr4.String(), Permissions: exchange.AllPermissions()}, + }, + }, + }, + Orders: []exchange.Order{ + askOrder(1, 1, s.addr1.String()), + bidOrder(2, 1, s.addr2.String()), + bidOrder(8, 420, s.addr2.String()), + bidOrder(77, 1, s.addr3.String()), + askOrder(79, 420, s.addr3.String()), + askOrder(1101, 1, s.addr4.String()), + }, + LastMarketId: 66, + LastOrderId: 5555, + }, + expAccCalls: AccountCalls{ + GetAccount: []sdk.AccAddress{s.marketAddr1, exchange.GetMarketAddress(420)}, + SetAccount: []authtypes.AccountI{marketAcc(1, "First Market"), marketAcc(420, "THE Market")}, + NewAccount: []authtypes.AccountI{marketAcc(1, "First Market"), marketAcc(420, "THE Market")}, + }, + expHoldCalls: HoldCalls{ + GetHoldCoin: []*GetHoldCoinArgs{ + {addr: s.addr1, denom: assetDenom}, {addr: s.addr1, denom: feeDenom}, + {addr: s.addr2, denom: feeDenom}, {addr: s.addr2, denom: priceDenom}, + {addr: s.addr3, denom: assetDenom}, {addr: s.addr3, denom: feeDenom}, {addr: s.addr3, denom: priceDenom}, + {addr: s.addr4, denom: assetDenom}, {addr: s.addr4, denom: feeDenom}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + origGenState := s.copyGenState(tc.genState) + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + if tc.expGenState == nil && len(tc.expInitPanic) == 0 { + tc.expGenState = s.sortGenState(s.copyGenState(tc.genState)) + } + if tc.expGenState == nil { + tc.expGenState = &exchange.GenesisState{} + } + + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + kpr := s.k.WithAccountKeeper(tc.accKeeper).WithHoldKeeper(tc.holdKeeper) + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + testInit := func() { + kpr.InitGenesis(ctx, tc.genState) + } + s.requirePanicEquals(testInit, tc.expInitPanic, "InitGenesis") + s.Assert().Equal(origGenState, tc.genState, "GenState before (expected) and after (actual) InitGenesis") + events := em.Events() + s.assertEqualEvents(nil, events, "events emitted during InitGenesis") + s.assertAccountKeeperCalls(tc.accKeeper, tc.expAccCalls, "InitGenesis") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "InitGenesis") + if len(tc.expInitPanic) > 0 { + return + } + + s.logBuffer.Reset() + var actGenState *exchange.GenesisState + testExport := func() { + actGenState = kpr.ExportGenesis(s.ctx) + } + s.Require().NotPanics(testExport, "ExportGenesis") + s.Assert().Equal(tc.expGenState, actGenState, "ExportGenesis") + actExportLog := s.getLogOutput("ExportGenesis") + s.Assert().Equal(tc.expExportLog, actExportLog, "things logged during ExportGenesis") + }) + } +} diff --git a/x/exchange/keeper/grpc_query.go b/x/exchange/keeper/grpc_query.go index 6824bf1967..3121452f66 100644 --- a/x/exchange/keeper/grpc_query.go +++ b/x/exchange/keeper/grpc_query.go @@ -42,6 +42,9 @@ func (k QueryServer) OrderFeeCalc(goCtx context.Context, req *exchange.QueryOrde switch { case req.AskOrder != nil: order := req.AskOrder + if err := validateMarketExists(store, order.MarketId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } ratioFee, err := calculateSellerSettlementRatioFee(store, order.MarketId, order.Price) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to calculate seller ratio fee option: %v", err) @@ -53,6 +56,9 @@ func (k QueryServer) OrderFeeCalc(goCtx context.Context, req *exchange.QueryOrde resp.CreationFeeOptions = getCreateAskFlatFees(store, order.MarketId) case req.BidOrder != nil: order := req.BidOrder + if err := validateMarketExists(store, order.MarketId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } ratioFees, err := calcBuyerSettlementRatioFeeOptions(store, order.MarketId, order.Price) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to calculate buyer ratio fee options: %v", err) @@ -93,6 +99,9 @@ func (k QueryServer) GetOrderByExternalID(goCtx context.Context, req *exchange.Q if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } + if req.MarketId == 0 || len(req.ExternalId) == 0 { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } ctx := sdk.UnwrapSDKContext(goCtx) order, err := k.Keeper.GetOrderByExternalID(ctx, req.MarketId, req.ExternalId) @@ -115,11 +124,10 @@ func (k QueryServer) GetMarketOrders(goCtx context.Context, req *exchange.QueryG ctx := sdk.UnwrapSDKContext(goCtx) pre := GetIndexKeyPrefixMarketToOrder(req.MarketId) - store := prefix.NewStore(k.getStore(ctx), pre) resp := &exchange.QueryGetMarketOrdersResponse{} var err error - resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(store, req.Pagination, req.OrderType, req.AfterOrderId) + resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(ctx, pre, req.Pagination, req.OrderType, req.AfterOrderId) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error iterating orders for market %d: %v", req.MarketId, err) @@ -136,16 +144,15 @@ func (k QueryServer) GetOwnerOrders(goCtx context.Context, req *exchange.QueryGe owner, aErr := sdk.AccAddressFromBech32(req.Owner) if aErr != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid owner: %v", aErr) + return nil, status.Errorf(codes.InvalidArgument, "invalid owner %q: %v", req.Owner, aErr) } ctx := sdk.UnwrapSDKContext(goCtx) pre := GetIndexKeyPrefixAddressToOrder(owner) - store := prefix.NewStore(k.getStore(ctx), pre) resp := &exchange.QueryGetOwnerOrdersResponse{} var err error - resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(store, req.Pagination, req.OrderType, req.AfterOrderId) + resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(ctx, pre, req.Pagination, req.OrderType, req.AfterOrderId) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error iterating orders for owner %s: %v", req.Owner, err) @@ -162,11 +169,10 @@ func (k QueryServer) GetAssetOrders(goCtx context.Context, req *exchange.QueryGe ctx := sdk.UnwrapSDKContext(goCtx) pre := GetIndexKeyPrefixAssetToOrder(req.Asset) - store := prefix.NewStore(k.getStore(ctx), pre) resp := &exchange.QueryGetAssetOrdersResponse{} var err error - resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(store, req.Pagination, req.OrderType, req.AfterOrderId) + resp.Pagination, resp.Orders, err = k.getPageOfOrdersFromIndex(ctx, pre, req.Pagination, req.OrderType, req.AfterOrderId) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error iterating orders for asset %s: %v", req.Asset, err) @@ -198,7 +204,7 @@ func (k QueryServer) GetAllOrders(goCtx context.Context, req *exchange.QueryGetA // Only add it to the result if we can read it. This might result in fewer results than the limit, // but at least one bad entry won't block others by causing the whole thing to return an error. order, oerr := k.parseOrderStoreValue(orderID, value) - if oerr != nil { + if oerr == nil { resp.Orders = append(resp.Orders, order) } } @@ -224,7 +230,12 @@ func (k QueryServer) GetMarket(goCtx context.Context, req *exchange.QueryGetMark return nil, status.Errorf(codes.InvalidArgument, "market %d not found", req.MarketId) } - return &exchange.QueryGetMarketResponse{Market: market}, nil + resp := &exchange.QueryGetMarketResponse{ + Address: exchange.GetMarketAddress(req.MarketId).String(), + Market: market, + } + + return resp, nil } // GetAllMarkets returns brief information about each market. diff --git a/x/exchange/keeper/grpc_query_test.go b/x/exchange/keeper/grpc_query_test.go index c3aa0cfa80..a3725efb1b 100644 --- a/x/exchange/keeper/grpc_query_test.go +++ b/x/exchange/keeper/grpc_query_test.go @@ -1,27 +1,4017 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestQueryServer_OrderFeeCalc() +import ( + "context" + "fmt" + "strings" -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetOrder() + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetOrderByExternalID() + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetMarketOrders() +const invalidArgErr = "rpc error: code = InvalidArgument" -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetOwnerOrders() +// logKeyAsID will log the provided key as either an order id or market id if it's suspected of being one of those. +func (s *TestSuite) logKeyAsID(name string, key []byte) { + switch len(key) { + case 8: + id, ok := keeper.ParseIndexKeySuffixOrderID(key) + if ok { + s.T().Logf("%s as order id: %d", name, id) + } + case 4: + id, ok := keeper.ParseKeySuffixKnownMarketID(key) + if ok { + s.T().Logf("%s as market id: %d", name, id) + } + } +} -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetAssetOrders() +// assertEqualPageResponse asserts that two PageResponses are equal. +// If not, some further assertions are made to try to help clarify the differences in the failure messages. +func (s *TestSuite) assertEqualPageResponse(expected, actual *query.PageResponse, msg string, args ...interface{}) bool { + s.T().Helper() + if s.Assert().Equalf(expected, actual, msg, args...) { + return true + } + if expected == nil || actual == nil { + return false + } + if !s.Assert().Equalf(expected.NextKey, actual.NextKey, msg+" NextKey", args...) { + s.logKeyAsID("Expected", expected.NextKey) + s.logKeyAsID(" Actual", actual.NextKey) + } + s.Assert().Equalf(int(expected.Total), int(actual.Total), msg+" Total", args...) + return false +} -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetAllOrders() +// queryTestDef is the definition of a QueryServer endpoint to be tested. +// R is the request message type. S is the response message type. +type queryTestDef[R any, S any] struct { + // queryName is the name of the query being tested. + queryName string + // query is the query function to invoke. + query func(goCtx context.Context, req *R) (*S, error) + // followup is a function that runs any desired followup assertions to help pinpoint + // differences between the expected and actual. It's only called if they're not equal and neither are nil. + followup func(expected, actual *S) +} -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetMarket() +// queryTestCase is a test case for a QueryServer endpoint. +// R is the request message type. S is the response message type. +type queryTestCase[R any, S any] struct { + // name is the name of the test case. + name string + // setup is a function that does any needed app/state setup. + // A cached context is used for tests, so this setup will not carry over between test cases. + setup func() + // req is the request message to provide to the query. + req *R + // expResp is the expected response from the query + expResp *S + // expInErr is the strings that are expected to be in the error returned by the endpoint. + // If empty, that error is expected to be nil. + expInErr []string +} -// TODO[1658]: func (s *TestSuite) TestQueryServer_GetAllMarkets() +// runQueryTestCase runs a unit test on a QueryServer endpoint. +// A cached context is used so each test case won't affect the others. +// R is the request message type. S is the response message type. +func runQueryTestCase[R any, S any](s *TestSuite, td queryTestDef[R, S], tc queryTestCase[R, S]) { + origCtx := s.ctx + defer func() { + s.ctx = origCtx + }() + s.ctx, _ = s.ctx.CacheContext() -// TODO[1658]: func (s *TestSuite) TestQueryServer_Params() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestQueryServer_ValidateCreateMarket() + goCtx := sdk.WrapSDKContext(s.ctx) + var resp *S + var err error + testFunc := func() { + resp, err = td.query(goCtx, tc.req) + } + s.Require().NotPanics(testFunc, td.queryName) + s.assertErrorContentsf(err, tc.expInErr, "%s error", td.queryName) + if s.Assert().Equal(tc.expResp, resp, "%s response", td.queryName) { + return + } + if td.followup != nil && tc.expResp != nil && resp != nil { + td.followup(tc.expResp, resp) + } +} -// TODO[1658]: func (s *TestSuite) TestQueryServer_ValidateMarket() +func (s *TestSuite) TestQueryServer_OrderFeeCalc() { + testDef := queryTestDef[exchange.QueryOrderFeeCalcRequest, exchange.QueryOrderFeeCalcResponse]{ + queryName: "OrderFeeCalc", + query: keeper.NewQueryServer(s.k).OrderFeeCalc, + followup: func(expected, actual *exchange.QueryOrderFeeCalcResponse) { + s.Assert().Equal(s.coinsString(expected.CreationFeeOptions), s.coinsString(actual.CreationFeeOptions), + "CreationFeeOptions (as strings)") + s.Assert().Equal(s.coinsString(expected.SettlementFlatFeeOptions), s.coinsString(actual.SettlementFlatFeeOptions), + "SettlementFlatFeeOptions (as strings)") + s.Assert().Equal(s.coinsString(expected.SettlementRatioFeeOptions), s.coinsString(actual.SettlementRatioFeeOptions), + "SettlementRatioFeeOptions (as strings)") + }, + } -// TODO[1658]: func (s *TestSuite) TestQueryServer_ValidateManageFees() + tests := []queryTestCase[exchange.QueryOrderFeeCalcRequest, exchange.QueryOrderFeeCalcResponse]{ + // Bad request tests. + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty req", + req: &exchange.QueryOrderFeeCalcRequest{}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "both order types", + req: &exchange.QueryOrderFeeCalcRequest{ + AskOrder: &exchange.AskOrder{MarketId: 1}, + BidOrder: &exchange.BidOrder{MarketId: 1}, + }, + expInErr: []string{invalidArgErr, "invalid request"}, + }, + + // AskOrder tests. + { + name: "ask: unknown market", + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2plum"), MarketId: 99, + }}, + expInErr: []string{invalidArgErr, "market 99 does not exist"}, + }, + { + name: "ask: no fees", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1}) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{}, + }, + { + name: "ask: only creation: one option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + FeeCreateAskFlat: s.coins("3fig"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("3fig"), + }, + }, + { + name: "ask: only creation: three options", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 8, + FeeCreateAskFlat: s.coins("3fig,52grape,1honeydew"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 8, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("3fig,52grape,1honeydew"), + }, + }, + { + name: "ask: only settlement flat: one option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 8, + FeeSellerSettlementFlat: s.coins("8grape"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 8, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("8grape"), + }, + }, + { + name: "ask: only settlement flat: three option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 8, + FeeSellerSettlementFlat: s.coins("23fig,6grape,15pineapple"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 8, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("23fig,6grape,15pineapple"), + }, + }, + { + name: "ask: price denom without ratio", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 4, + FeeSellerSettlementRatios: s.ratios("500plum:3plum"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000peach"), MarketId: 4, + }}, + expInErr: []string{invalidArgErr, "failed to calculate seller ratio fee option", + "no seller settlement fee ratio found for denom \"peach\""}, + }, + { + name: "ask: only settlement ratio", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 8, + FeeSellerSettlementRatios: s.ratios("500plum:3plum"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 8, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementRatioFeeOptions: s.coins("12plum"), + }, + }, + { + name: "ask: both settlement", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 8, + FeeSellerSettlementFlat: s.coins("23fig,6grape,15pineapple"), + FeeSellerSettlementRatios: s.ratios("500plum:3plum"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 8, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("23fig,6grape,15pineapple"), + SettlementRatioFeeOptions: s.coins("12plum"), + }, + }, + { + name: "ask: all fees", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + FeeCreateAskFlat: s.coins("3fig,52grape,1honeydew"), + FeeSellerSettlementFlat: s.coins("23fig,6grape,15pineapple"), + FeeSellerSettlementRatios: s.ratios("500plum:3plum"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{AskOrder: &exchange.AskOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("3fig,52grape,1honeydew"), + SettlementFlatFeeOptions: s.coins("23fig,6grape,15pineapple"), + SettlementRatioFeeOptions: s.coins("12plum"), + }, + }, + + // BidOrder tests. + { + name: "bid: unknown market", + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 33, + }}, + expInErr: []string{invalidArgErr, "market 33 does not exist"}, + }, + { + name: "bid: no fees", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1}) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{}, + }, + { + name: "bid: only creation: one option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 33, + FeeCreateBidFlat: s.coins("7honeydew"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 33, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("7honeydew"), + }, + }, + { + name: "bid: only creation: three options", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 33, + FeeCreateBidFlat: s.coins("57fig,6honeydew,223jackfruit"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 33, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("57fig,6honeydew,223jackfruit"), + }, + }, + { + name: "bid: only settlement flat: one option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + FeeBuyerSettlementFlat: s.coins("12pineapple"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 3, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("12pineapple"), + }, + }, + { + name: "bid: only settlement flat: three options", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + FeeBuyerSettlementFlat: s.coins("7peach,12pineapple,66plum"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 3, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("7peach,12pineapple,66plum"), + }, + }, + { + name: "bid: price denom without ratio", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + FeeBuyerSettlementRatios: s.ratios("1000peach:3fig"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expInErr: []string{invalidArgErr, "failed to calculate buyer ratio fee options", + "no buyer settlement fee ratios found for denom \"plum\""}, + }, + { + name: "bid: no applicable ratios for price amount", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeBuyerSettlementRatios: s.ratios("1000plum:3fig,750plum:66grape,500plum:1honeydew"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("5737plum"), MarketId: 7, + }}, + expInErr: []string{invalidArgErr, "failed to calculate buyer ratio fee options", + "cannot apply ratio 1000plum:3fig to price 5737plum", + "cannot apply ratio 750plum:66grape to price 5737plum", + "cannot apply ratio 500plum:1honeydew to price 5737plum", + "no applicable buyer settlement fee ratios found for price 5737plum", + }, + }, + { + name: "bid: only settlement ratio: one option", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + FeeBuyerSettlementRatios: s.ratios("1000plum:3fig"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementRatioFeeOptions: s.coins("6fig"), + }, + }, + { + name: "bid: only settlement ratio: three options", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + s.ratio("1000plum:3fig"), + s.ratio("751plum:33grape"), // cannot be applied to given price. + s.ratio("1apple:10honeydew"), // cannot be applied to given price. + s.ratio("2000plum:5peach"), + s.ratio("500plum:1plum"), + }, + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 1, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementRatioFeeOptions: s.coins("6fig,5peach,4plum"), + }, + }, + { + name: "bid: both settlement", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + FeeBuyerSettlementFlat: s.coins("12fig,15grape"), + FeeBuyerSettlementRatios: s.ratios("1000plum:3fig,1000plum:4grape"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 2, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + SettlementFlatFeeOptions: s.coins("12fig,15grape"), + SettlementRatioFeeOptions: s.coins("6fig,8grape"), + }, + }, + { + name: "bid: all fees", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + FeeCreateBidFlat: s.coins("77fig,88grape"), + FeeBuyerSettlementFlat: s.coins("12fig,15grape"), + FeeBuyerSettlementRatios: s.ratios("1000plum:3fig,1000plum:4grape"), + }) + }, + req: &exchange.QueryOrderFeeCalcRequest{BidOrder: &exchange.BidOrder{ + Assets: s.coin("1apple"), Price: s.coin("2000plum"), MarketId: 3, + }}, + expResp: &exchange.QueryOrderFeeCalcResponse{ + CreationFeeOptions: s.coins("77fig,88grape"), + SettlementFlatFeeOptions: s.coins("12fig,15grape"), + SettlementRatioFeeOptions: s.coins("6fig,8grape"), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetOrder() { + testDef := queryTestDef[exchange.QueryGetOrderRequest, exchange.QueryGetOrderResponse]{ + queryName: "GetOrder", + query: keeper.NewQueryServer(s.k).GetOrder, + } + + tests := []queryTestCase[exchange.QueryGetOrderRequest, exchange.QueryGetOrderResponse]{ + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "order 0", + req: &exchange.QueryGetOrderRequest{OrderId: 0}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "error getting order", + setup: func() { + key, value, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(4).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("55apple"), + Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 4") + value[0] = 9 + s.getStore().Set(key, value) + }, + req: &exchange.QueryGetOrderRequest{OrderId: 4}, + expInErr: []string{invalidArgErr, "failed to read order 4: unknown type byte 0x9"}, + }, + { + name: "order not found", + req: &exchange.QueryGetOrderRequest{OrderId: 4}, + expInErr: []string{invalidArgErr, "order 4 not found"}, + }, + { + name: "order 1: ask", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("20apple"), + Price: s.coin("3pineapple"), + SellerSettlementFlatFee: s.coinP("15fig"), + AllowPartial: true, + ExternalId: "ask-order-1-id", + })) + }, + req: &exchange.QueryGetOrderRequest{OrderId: 1}, + expResp: &exchange.QueryGetOrderResponse{Order: exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("20apple"), + Price: s.coin("3pineapple"), + SellerSettlementFlatFee: s.coinP("15fig"), + AllowPartial: true, + ExternalId: "ask-order-1-id", + })}, + }, + { + name: "order 1: bid", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("20apple"), + Price: s.coin("3pineapple"), + BuyerSettlementFees: s.coins("15fig,10grape"), + AllowPartial: true, + ExternalId: "ask-order-1-id", + })) + }, + req: &exchange.QueryGetOrderRequest{OrderId: 1}, + expResp: &exchange.QueryGetOrderResponse{Order: exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("20apple"), + Price: s.coin("3pineapple"), + BuyerSettlementFees: s.coins("15fig,10grape"), + AllowPartial: true, + ExternalId: "ask-order-1-id", + })}, + }, + { + name: "order 5555", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(5554).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), + Assets: s.coin("20apple"), Price: s.coin("3pineapple"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5555).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr2.String(), + Assets: s.coin("77acorn"), Price: s.coin("453prune"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5556).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr3.String(), + Assets: s.coin("55acai"), Price: s.coin("77peach"), + })) + }, + req: &exchange.QueryGetOrderRequest{OrderId: 5555}, + expResp: &exchange.QueryGetOrderResponse{Order: exchange.NewOrder(5555).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr2.String(), + Assets: s.coin("77acorn"), Price: s.coin("453prune"), + })}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetOrderByExternalID() { + testDef := queryTestDef[exchange.QueryGetOrderByExternalIDRequest, exchange.QueryGetOrderByExternalIDResponse]{ + queryName: "GetOrderByExternalID", + query: keeper.NewQueryServer(s.k).GetOrderByExternalID, + } + + tests := []queryTestCase[exchange.QueryGetOrderByExternalIDRequest, exchange.QueryGetOrderByExternalIDResponse]{ + { + name: "nil request", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "market 0", + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 0, ExternalId: "something"}, + expInErr: []string{invalidArgErr, "invalid request"}, + }, + { + name: "no external id", + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 1, ExternalId: ""}, + expInErr: []string{invalidArgErr, "invalid request"}, + }, + { + name: "error getting order", + setup: func() { + order5 := exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr3.String(), + Assets: s.coin("1apple"), + Price: s.coin("1plum"), + BuyerSettlementFees: nil, + AllowPartial: false, + ExternalId: "babbaderr", + }) + store := s.getStore() + // Save it normally to get the indexes with it, then overwite the value with a bad one. + s.requireSetOrderInStore(store, order5) + key5, value5, err := s.k.GetOrderStoreKeyValue(*order5) + s.Require().NoError(err, "GetOrderStoreKeyValue 5") + value5[0] = 9 + store.Set(key5, value5) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 1, ExternalId: "babbaderr"}, + expInErr: []string{invalidArgErr, "failed to read order 5: unknown type byte 0x9"}, + }, + { + name: "no such order", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr2.String(), + Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "nosuchorder", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr2.String(), + Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "nosuchorder", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 2, ExternalId: "nosuchorder"}, + expInErr: []string{invalidArgErr, "order not found in market 2 with external id \"nosuchorder\""}, + }, + { + name: "only one order with the id: ask", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(77).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "myspecialid", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 3, ExternalId: "myspecialid"}, + expResp: &exchange.QueryGetOrderByExternalIDResponse{Order: exchange.NewOrder(77).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "myspecialid", + })}, + }, + { + name: "only one order with the id: bid", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 999, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "mycoolid", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 999, ExternalId: "mycoolid"}, + expResp: &exchange.QueryGetOrderByExternalIDResponse{Order: exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 999, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "mycoolid", + })}, + }, + { + name: "three markets with same id: first", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(56).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("3apple"), Price: s.coin("3plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid1", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(58).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid2", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 88, ExternalId: "commonid"}, + expResp: &exchange.QueryGetOrderByExternalIDResponse{Order: exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "commonid", + })}, + }, + { + name: "three markets with same id: second", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(56).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("3apple"), Price: s.coin("3plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid1", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(58).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid2", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 2, ExternalId: "commonid"}, + expResp: &exchange.QueryGetOrderByExternalIDResponse{Order: exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2plum"), + ExternalId: "commonid", + })}, + }, + { + name: "three markets with same id: second", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 88, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(56).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("3apple"), Price: s.coin("3plum"), + ExternalId: "commonid", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid1", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(58).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("1apple"), Price: s.coin("1plum"), + ExternalId: "otherid2", + })) + }, + req: &exchange.QueryGetOrderByExternalIDRequest{MarketId: 9001, ExternalId: "commonid"}, + expResp: &exchange.QueryGetOrderByExternalIDResponse{Order: exchange.NewOrder(56).WithBid(&exchange.BidOrder{ + MarketId: 9001, Buyer: s.addr2.String(), Assets: s.coin("3apple"), Price: s.coin("3plum"), + ExternalId: "commonid", + })}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetMarketOrders() { + testDef := queryTestDef[exchange.QueryGetMarketOrdersRequest, exchange.QueryGetMarketOrdersResponse]{ + queryName: "GetMarketOrders", + query: keeper.NewQueryServer(s.k).GetMarketOrders, + followup: func(expected, actual *exchange.QueryGetMarketOrdersResponse) { + s.assertEqualOrders(expected.Orders, actual.Orders, "Orders") + s.assertEqualPageResponse(expected.Pagination, actual.Pagination, "Pagination") + }, + } + makeKey := func(order *exchange.Order) []byte { + return keeper.Uint64Bz(order.OrderId) + } + + marketCount, ordersPerMarket := 3, 20 + marketOrders := make(map[uint32][]*exchange.Order, marketCount) + marketAskOrders := make(map[uint32][]*exchange.Order, marketCount) + marketBidOrders := make(map[uint32][]*exchange.Order, marketCount) + for m := uint32(1); m <= uint32(marketCount); m++ { + marketOrders[m] = make([]*exchange.Order, 0, ordersPerMarket) + marketAskOrders[m] = make([]*exchange.Order, 0, ordersPerMarket/2) + marketBidOrders[m] = make([]*exchange.Order, 0, ordersPerMarket/2) + } + mainStore := s.getStore() + for i := 1; i <= marketCount*ordersPerMarket; i++ { + orderID := uint64(i) + marketID := uint32((i-1)%marketCount) + 1 + order := exchange.NewOrder(orderID) + if orderID%2 == 0 { + order.WithAsk(&exchange.AskOrder{ + MarketId: marketID, + Seller: sdk.AccAddress(fmt.Sprintf("seller_%d____________", orderID)[:20]).String(), + Assets: sdk.NewInt64Coin("apple", int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + marketAskOrders[marketID] = append(marketAskOrders[marketID], order) + } else { + order.WithBid(&exchange.BidOrder{ + MarketId: marketID, + Buyer: sdk.AccAddress(fmt.Sprintf("buyer_%d_____________", orderID)[:20]).String(), + Assets: sdk.NewInt64Coin("apple", int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + marketBidOrders[marketID] = append(marketBidOrders[marketID], order) + } + marketOrders[marketID] = append(marketOrders[marketID], order) + s.requireSetOrderInStore(mainStore, order) + } + + // OrderIDs in each market: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + //1: 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58 + //2: 2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59 + //3: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60 + + tests := []queryTestCase[exchange.QueryGetMarketOrdersRequest, exchange.QueryGetMarketOrdersResponse]{ + // Tests on errors and non-normal conditions. + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "market 0", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 0}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "unknown order type", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 4, OrderType: "burger and fries"}, + expInErr: []string{invalidArgErr, "error iterating orders for market 4: unknown order type \"burger and fries\""}, + }, + { + name: "no orders", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 8}, + expResp: &exchange.QueryGetMarketOrdersResponse{Orders: nil, Pagination: &query.PageResponse{}}, + }, + { + name: "bad index entry", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr2.String(), Assets: s.coin("99apple"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + store.Set(key99, value99) + store.Set(s.badKey(keeper.MakeIndexKeyMarketToOrder(8, 99)), []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 8}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 2}, + }, + }, + { + name: "index entry to order that does not exist", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key := keeper.MakeIndexKeyMarketToOrder(8, 99) + store.Set(key, []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 8}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "error reading an order", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr2.String(), Assets: s.coin("99apple"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + value99[0] = 8 + store.Set(key99, value99) + idxKey := keeper.MakeIndexKeyMarketToOrder(8, 99) + store.Set(idxKey, []byte{8}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 8}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "both offset and key provided", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: &query.PageRequest{Offset: 2, Key: makeKey(marketOrders[1][2])}, + }, + expInErr: []string{invalidArgErr, "error iterating orders for market 1", + "invalid request, either offset or key is expected, got both"}, + }, + + // Forward, no order type. + { + name: "forward, no order type, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 1}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[1], + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "forward, no order type, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(marketOrders[2][2])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[2][2:5], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[2][5])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, + Pagination: &query.PageRequest{Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[3][8:13], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[3][13])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: &query.PageRequest{Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[1][6:11], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][11]), Total: 20}, + }, + }, + { + name: "forward, no order type, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 2, AfterOrderId: 30}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[2][10:], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, no order type, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Key: makeKey(marketOrders[1][15])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[1][15:17], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][17])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[1][12:15], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][15])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketOrders[3][17:18], + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[3][18]), Total: 10}, + }, + }, + + // Forward, only ask orders + { + name: "forward, ask orders, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 3, OrderType: "ask"}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, ask orders, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "asks", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(marketAskOrders[1][4])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][7])}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ASK", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ASKS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[2][9]), Total: 10}, + }, + }, + { + name: "forward, ask orders, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 3, OrderType: "AskOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(marketAskOrders[2][7])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[2][8])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][9])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketAskOrders[1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][8]), Total: 5}, + }, + }, + + // Forward, only bid orders + { + name: "forward, bid orders, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 3, OrderType: "bid"}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, bid orders, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bids", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(marketBidOrders[1][4])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][7])}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "BID", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "BIDS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[2][9]), Total: 10}, + }, + }, + { + name: "forward, bid orders, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{MarketId: 3, OrderType: "BidOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(marketBidOrders[2][7])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[2][8])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][9])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: marketBidOrders[1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][8]), Total: 5}, + }, + }, + + // Reverse, no order type. + { + name: "reverse, no order type, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[1]), + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "reverse, no order type, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(marketOrders[2][12])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[2][10:13]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[2][9])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[3][7:12]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[3][6])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[1][9:14]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][8]), Total: 20}, + }, + }, + { + name: "reverse, no order type, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[2][10:]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Key: makeKey(marketOrders[1][15])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[1][14:16]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][13])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[1][15:18]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[1][14])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset and count", + // A key point of this test is that order 30 is in market 3. The AfterOrderID order + // should NOT be included in results, though, so there should still only be 10 results here. + // This validates that the "afterOrderID + 1" is correct in the getOrderIterator reverse block. + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketOrders[3][12:13]), + Pagination: &query.PageResponse{NextKey: makeKey(marketOrders[3][11]), Total: 10}, + }, + }, + + // Reverse, only ask orders + { + name: "reverse, ask orders, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, OrderType: "ask", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "asks", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(marketAskOrders[1][4])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][1])}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ASK", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ASKS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[2][5]), Total: 10}, + }, + }, + { + name: "reverse, ask orders, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, OrderType: "AskOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(marketAskOrders[2][7])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[2][6])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][5])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketAskOrders[1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(marketAskOrders[1][5]), Total: 5}, + }, + }, + + // Reverse, only bid orders + { + name: "reverse, bid orders, no after order, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, OrderType: "bid", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bids", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(marketBidOrders[1][4])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][1])}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "BID", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "BIDS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[2][5]), Total: 10}, + }, + }, + { + name: "reverse, bid orders, after order 30, get all", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 3, OrderType: "BidOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with key", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(marketBidOrders[2][7])}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[2][6])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][5])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetMarketOrdersResponse{ + Orders: reverseSlice(marketBidOrders[1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(marketBidOrders[1][5]), Total: 5}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetOwnerOrders() { + testDef := queryTestDef[exchange.QueryGetOwnerOrdersRequest, exchange.QueryGetOwnerOrdersResponse]{ + queryName: "GetOwnerOrders", + query: keeper.NewQueryServer(s.k).GetOwnerOrders, + followup: func(expected, actual *exchange.QueryGetOwnerOrdersResponse) { + s.assertEqualOrders(expected.Orders, actual.Orders, "Orders") + s.assertEqualPageResponse(expected.Pagination, actual.Pagination, "Pagination") + }, + } + makeKey := func(order *exchange.Order) []byte { + return keeper.Uint64Bz(order.OrderId) + } + + addr1, addr2, addr3 := s.addr1.String(), s.addr2.String(), s.addr3.String() + owners := []string{addr1, addr2, addr3} + ownerCount := len(owners) + ordersPerOwner := 20 + ownerOrders := make(map[string][]*exchange.Order, ownerCount) + ownerAskOrders := make(map[string][]*exchange.Order, ownerCount) + ownerBidOrders := make(map[string][]*exchange.Order, ownerCount) + for _, owner := range owners { + ownerOrders[owner] = make([]*exchange.Order, 0, ordersPerOwner) + ownerAskOrders[owner] = make([]*exchange.Order, 0, ordersPerOwner/2) + ownerBidOrders[owner] = make([]*exchange.Order, 0, ordersPerOwner/2) + } + mainStore := s.getStore() + for i := 1; i <= ownerCount*ordersPerOwner; i++ { + orderID := uint64(i) + owner := owners[i%ownerCount] + order := exchange.NewOrder(orderID) + if orderID%2 == 0 { + order.WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: owner, + Assets: sdk.NewInt64Coin("apple", int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + ownerAskOrders[owner] = append(ownerAskOrders[owner], order) + } else { + order.WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: owner, + Assets: sdk.NewInt64Coin("apple", int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + ownerBidOrders[owner] = append(ownerBidOrders[owner], order) + } + ownerOrders[owner] = append(ownerOrders[owner], order) + s.requireSetOrderInStore(mainStore, order) + } + + // OrderIDs for each owner: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + //addr1: 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58 + //addr2: 2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59 + //addr3: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60 + + tests := []queryTestCase[exchange.QueryGetOwnerOrdersRequest, exchange.QueryGetOwnerOrdersResponse]{ + // Tests on errors and non-normal conditions. + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty owner", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: ""}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "invalid owner", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: "notgonnawork"}, + expInErr: []string{invalidArgErr, "invalid owner \"notgonnawork\"", "decoding bech32 failed"}, + }, + { + name: "unknown order type", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr1, OrderType: "burger and fries"}, + expInErr: []string{invalidArgErr, "error iterating orders for owner " + addr1 + ": unknown order type \"burger and fries\""}, + }, + { + name: "no orders", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: s.addr4.String()}, + expResp: &exchange.QueryGetOwnerOrdersResponse{Orders: nil, Pagination: &query.PageResponse{}}, + }, + { + name: "bad index entry", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("99apple"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + store.Set(key99, value99) + store.Set(s.badKey(keeper.MakeIndexKeyAddressToOrder(s.addr4, 99)), []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetOwnerOrdersRequest{Owner: s.addr4.String()}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 2}, + }, + }, + { + name: "index entry to order that does not exist", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key := keeper.MakeIndexKeyAddressToOrder(s.addr4, 99) + store.Set(key, []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetOwnerOrdersRequest{Owner: s.addr4.String()}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr4.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "error reading an order", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr5.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr5.String(), Assets: s.coin("99apple"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + value99[0] = 8 + store.Set(key99, value99) + idxKey := keeper.MakeIndexKeyAddressToOrder(s.addr5, 99) + store.Set(idxKey, []byte{8}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr5.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetOwnerOrdersRequest{Owner: s.addr5.String()}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr5.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr5.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "both offset and key provided", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, + Pagination: &query.PageRequest{Offset: 2, Key: makeKey(ownerOrders[addr1][2])}, + }, + expInErr: []string{invalidArgErr, "error iterating orders for owner " + addr1, + "invalid request, either offset or key is expected, got both"}, + }, + + // Forward, no order type. + { + name: "forward, no order type, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr1}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr1], + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "forward, no order type, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(ownerOrders[addr2][2])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr2][2:5], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr2][5])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, + Pagination: &query.PageRequest{Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr3][8:13], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr3][13])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, + Pagination: &query.PageRequest{Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr1][6:11], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][11]), Total: 20}, + }, + }, + { + name: "forward, no order type, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr2, AfterOrderId: 30}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr2][10:], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, no order type, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Key: makeKey(ownerOrders[addr1][15])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr1][15:17], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][17])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr1][12:15], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][15])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerOrders[addr3][17:18], + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr3][18]), Total: 10}, + }, + }, + + // Forward, only ask orders + { + name: "forward, ask orders, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr3, OrderType: "ask"}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, ask orders, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "asks", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(ownerAskOrders[addr1][4])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][7])}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ASK", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ASKS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr2][9]), Total: 10}, + }, + }, + { + name: "forward, ask orders, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr3, OrderType: "AskOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(ownerAskOrders[addr2][7])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr2][8])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][9])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerAskOrders[addr1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][8]), Total: 5}, + }, + }, + + // Forward, only bid orders + { + name: "forward, bid orders, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr3, OrderType: "bid"}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, bid orders, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bids", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(ownerBidOrders[addr1][4])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][7])}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "BID", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "BIDS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr2][9]), Total: 10}, + }, + }, + { + name: "forward, bid orders, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{Owner: addr3, OrderType: "BidOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(ownerBidOrders[addr2][7])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr2][8])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][9])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: ownerBidOrders[addr1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][8]), Total: 5}, + }, + }, + + // Reverse, no order type. + { + name: "reverse, no order type, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr1]), + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "reverse, no order type, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(ownerOrders[addr2][12])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr2][10:13]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr2][9])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr3][7:12]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr3][6])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr1][9:14]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][8]), Total: 20}, + }, + }, + { + name: "reverse, no order type, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr2][10:]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Key: makeKey(ownerOrders[addr1][15])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr1][14:16]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][13])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr1][15:18]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr1][14])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset and count", + // A key point of this test is that order 30 is in market 3. The AfterOrderID order + // should NOT be included in results, though, so there should still only be 10 results here. + // This validates that the "afterOrderID + 1" is correct in the getOrderIterator reverse block. + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerOrders[addr3][12:13]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerOrders[addr3][11]), Total: 10}, + }, + }, + + // Reverse, only ask orders + { + name: "reverse, ask orders, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, OrderType: "ask", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "asks", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(ownerAskOrders[addr1][4])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][1])}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ASK", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ASKS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr2][5]), Total: 10}, + }, + }, + { + name: "reverse, ask orders, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, OrderType: "AskOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(ownerAskOrders[addr2][7])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr2][6])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][5])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerAskOrders[addr1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerAskOrders[addr1][5]), Total: 5}, + }, + }, + + // Reverse, only bid orders + { + name: "reverse, bid orders, no after order, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, OrderType: "bid", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bids", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(ownerBidOrders[addr1][4])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][1])}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "BID", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "BIDS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr2][5]), Total: 10}, + }, + }, + { + name: "reverse, bid orders, after order 30, get all", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr3, OrderType: "BidOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with key", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(ownerBidOrders[addr2][7])}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr2][6])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][5])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetOwnerOrdersRequest{ + Owner: addr1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetOwnerOrdersResponse{ + Orders: reverseSlice(ownerBidOrders[addr1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(ownerBidOrders[addr1][5]), Total: 5}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetAssetOrders() { + testDef := queryTestDef[exchange.QueryGetAssetOrdersRequest, exchange.QueryGetAssetOrdersResponse]{ + queryName: "GetAssetOrders", + query: keeper.NewQueryServer(s.k).GetAssetOrders, + followup: func(expected, actual *exchange.QueryGetAssetOrdersResponse) { + s.assertEqualOrders(expected.Orders, actual.Orders, "Orders") + s.assertEqualPageResponse(expected.Pagination, actual.Pagination, "Pagination") + }, + } + makeKey := func(order *exchange.Order) []byte { + return keeper.Uint64Bz(order.OrderId) + } + + denom1, denom2, denom3 := "one", "two", "three" + denoms := []string{denom1, denom2, denom3} + denomCount := len(denoms) + ordersPerDenom := 20 + denomOrders := make(map[string][]*exchange.Order, denomCount) + denomAskOrders := make(map[string][]*exchange.Order, denomCount) + denomBidOrders := make(map[string][]*exchange.Order, denomCount) + for _, denom := range denoms { + denomOrders[denom] = make([]*exchange.Order, 0, ordersPerDenom) + denomAskOrders[denom] = make([]*exchange.Order, 0, ordersPerDenom/2) + denomBidOrders[denom] = make([]*exchange.Order, 0, ordersPerDenom/2) + } + mainStore := s.getStore() + for i := 1; i <= denomCount*ordersPerDenom; i++ { + orderID := uint64(i) + denom := denoms[i%denomCount] + order := exchange.NewOrder(orderID) + if orderID%2 == 0 { + order.WithAsk(&exchange.AskOrder{ + MarketId: uint32(5000 + i), + Seller: sdk.AccAddress(fmt.Sprintf("seller_%d____________", orderID)[:20]).String(), + Assets: sdk.NewInt64Coin(denom, int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + denomAskOrders[denom] = append(denomAskOrders[denom], order) + } else { + order.WithBid(&exchange.BidOrder{ + MarketId: uint32(5000 + i), + Buyer: sdk.AccAddress(fmt.Sprintf("buyer_%d_____________", orderID)[:20]).String(), + Assets: sdk.NewInt64Coin(denom, int64(i)), + Price: sdk.NewInt64Coin("plum", int64(i)), + AllowPartial: orderID%4 < 2, + ExternalId: fmt.Sprintf("external-id-%d", i), + }) + denomBidOrders[denom] = append(denomBidOrders[denom], order) + } + denomOrders[denom] = append(denomOrders[denom], order) + s.requireSetOrderInStore(mainStore, order) + } + + // OrderIDs for each denom: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + //denom1: 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58 + //denom2: 2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59 + //denom3: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60 + + tests := []queryTestCase[exchange.QueryGetAssetOrdersRequest, exchange.QueryGetAssetOrdersResponse]{ + // Tests on errors and non-normal conditions. + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty asset", + req: &exchange.QueryGetAssetOrdersRequest{Asset: ""}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "unknown order type", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom1, OrderType: "burger and fries"}, + expInErr: []string{invalidArgErr, "error iterating orders for asset " + denom1 + ": unknown order type \"burger and fries\""}, + }, + { + name: "no orders", + req: &exchange.QueryGetAssetOrdersRequest{Asset: "four"}, + expResp: &exchange.QueryGetAssetOrdersResponse{Orders: nil, Pagination: &query.PageResponse{}}, + }, + { + name: "bad index entry", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr2.String(), Assets: s.coin("99apple"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + store.Set(key99, value99) + store.Set(s.badKey(keeper.MakeIndexKeyMarketToOrder(8, 99)), []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetAssetOrdersRequest{Asset: "apple"}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98apple"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100apple"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 2}, + }, + }, + { + name: "index entry to order that does not exist", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98acorn"), Price: s.coin("98prune"), + })) + key := keeper.MakeIndexKeyAssetToOrder("acorn", 99) + store.Set(key, []byte{keeper.OrderKeyTypeAsk}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100acorn"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetAssetOrdersRequest{Asset: "acorn"}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98acorn"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100acorn"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "error reading an order", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98acorn"), Price: s.coin("98prune"), + })) + key99, value99, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(99).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr2.String(), Assets: s.coin("99acorn"), Price: s.coin("99prune"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 99") + value99[0] = 8 + store.Set(key99, value99) + idxKey := keeper.MakeIndexKeyAssetToOrder("acorn", 99) + store.Set(idxKey, []byte{8}) + s.requireSetOrderInStore(store, exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100acorn"), Price: s.coin("100prune"), + })) + }, + req: &exchange.QueryGetAssetOrdersRequest{Asset: "acorn"}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(98).WithAsk(&exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("98acorn"), Price: s.coin("98prune"), + }), + exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 9, Seller: s.addr3.String(), Assets: s.coin("100acorn"), Price: s.coin("100prune"), + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "both offset and key provided", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, + Pagination: &query.PageRequest{Offset: 2, Key: makeKey(denomOrders[denom1][2])}, + }, + expInErr: []string{invalidArgErr, "error iterating orders for asset " + denom1, + "invalid request, either offset or key is expected, got both"}, + }, + + // Forward, no order type. + { + name: "forward, no order type, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom1}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom1], + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "forward, no order type, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(denomOrders[denom2][2])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom2][2:5], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom2][5])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, + Pagination: &query.PageRequest{Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom3][8:13], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom3][13])}, + }, + }, + { + name: "forward, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, + Pagination: &query.PageRequest{Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom1][6:11], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][11]), Total: 20}, + }, + }, + { + name: "forward, no order type, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom2, AfterOrderId: 30}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom2][10:], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, no order type, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Key: makeKey(denomOrders[denom1][15])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom1][15:17], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][17])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom1][12:15], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][15])}, + }, + }, + { + name: "forward, no order type, after order 30, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomOrders[denom3][17:18], + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom3][18]), Total: 10}, + }, + }, + + // Forward, only ask orders + { + name: "forward, ask orders, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom3, OrderType: "ask"}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, ask orders, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "asks", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(denomAskOrders[denom1][4])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][7])}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ASK", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ASKS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom2][9]), Total: 10}, + }, + }, + { + name: "forward, ask orders, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom3, OrderType: "AskOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(denomAskOrders[denom2][7])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom2][8])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][9])}, + }, + }, + { + name: "forward, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomAskOrders[denom1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][8]), Total: 5}, + }, + }, + + // Forward, only bid orders + { + name: "forward, bid orders, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom3, OrderType: "bid"}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom3], + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "forward, bid orders, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bids", + Pagination: &query.PageRequest{Limit: 3, Key: makeKey(denomBidOrders[denom1][4])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom1][4:7], + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][7])}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "BID", + Pagination: &query.PageRequest{Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom2][8:], + Pagination: &query.PageResponse{}, + }, + }, + { + name: "forward, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "BIDS", + Pagination: &query.PageRequest{Limit: 3, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom2][6:9], + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom2][9]), Total: 10}, + }, + }, + { + name: "forward, bid orders, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{Asset: denom3, OrderType: "BidOrders", AfterOrderId: 30}, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom3][5:], + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 1, Key: makeKey(denomBidOrders[denom2][7])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom2][7:8], + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom2][8])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom1][7:9], + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][9])}, + }, + }, + { + name: "forward, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: denomBidOrders[denom1][6:8], + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][8]), Total: 5}, + }, + }, + + // Reverse, no order type. + { + name: "reverse, no order type, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom1]), + Pagination: &query.PageResponse{Total: 20}, + }, + }, + { + name: "reverse, no order type, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(denomOrders[denom2][12])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom2][10:13]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom2][9])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom3][7:12]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom3][6])}, + }, + }, + { + name: "reverse, no order type, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, + Pagination: &query.PageRequest{Reverse: true, Limit: 5, Offset: 6, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom1][9:14]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][8]), Total: 20}, + }, + }, + { + name: "reverse, no order type, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom2][10:]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Key: makeKey(denomOrders[denom1][15])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom1][14:16]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][13])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom1][15:18]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom1][14])}, + }, + }, + { + name: "reverse, no order type, after order 30, limit with offset and count", + // A key point of this test is that order 30 is in market 3. The AfterOrderID order + // should NOT be included in results, though, so there should still only be 10 results here. + // This validates that the "afterOrderID + 1" is correct in the getOrderIterator reverse block. + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Offset: 7, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomOrders[denom3][12:13]), + Pagination: &query.PageResponse{NextKey: makeKey(denomOrders[denom3][11]), Total: 10}, + }, + }, + + // Reverse, only ask orders + { + name: "reverse, ask orders, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, OrderType: "ask", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "asks", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(denomAskOrders[denom1][4])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][1])}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ASK", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, ask orders, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ASKS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom2][5]), Total: 10}, + }, + }, + { + name: "reverse, ask orders, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, OrderType: "AskOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "ask orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(denomAskOrders[denom2][7])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom2][6])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "askOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][5])}, + }, + }, + { + name: "reverse, ask orders, after order 30, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "aSKs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomAskOrders[denom1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(denomAskOrders[denom1][5]), Total: 5}, + }, + }, + + // Reverse, only bid orders + { + name: "reverse, bid orders, no after order, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, OrderType: "bid", + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom3]), + Pagination: &query.PageResponse{Total: 10}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bids", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Key: makeKey(denomBidOrders[denom1][4])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom1][2:5]), + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][1])}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "BID", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 8, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom2][:2]), + Pagination: &query.PageResponse{}, + }, + }, + { + name: "reverse, bid orders, no after order, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "BIDS", + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom2][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom2][5]), Total: 10}, + }, + }, + { + name: "reverse, bid orders, after order 30, get all", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom3, OrderType: "BidOrders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom3][5:]), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with key", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom2, OrderType: "bid orders", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 1, Key: makeKey(denomBidOrders[denom2][7])}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom2][7:8]), + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom2][6])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset, no count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bidOrder", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 2, Offset: 2, CountTotal: false}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom1][6:8]), + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][5])}, + }, + }, + { + name: "reverse, bid orders, after order 30, limit with offset and count", + req: &exchange.QueryGetAssetOrdersRequest{ + Asset: denom1, OrderType: "bIDs", AfterOrderId: 30, + Pagination: &query.PageRequest{Reverse: true, Limit: 3, Offset: 1, CountTotal: true}, + }, + expResp: &exchange.QueryGetAssetOrdersResponse{ + Orders: reverseSlice(denomBidOrders[denom1][6:9]), + Pagination: &query.PageResponse{NextKey: makeKey(denomBidOrders[denom1][5]), Total: 5}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetAllOrders() { + testDef := queryTestDef[exchange.QueryGetAllOrdersRequest, exchange.QueryGetAllOrdersResponse]{ + queryName: "GetAllOrders", + query: keeper.NewQueryServer(s.k).GetAllOrders, + followup: func(expected, actual *exchange.QueryGetAllOrdersResponse) { + s.assertEqualOrders(expected.Orders, actual.Orders, "Orders") + s.assertEqualPageResponse(expected.Pagination, actual.Pagination, "Pagination") + }, + } + makeKey := func(order *exchange.Order) []byte { + return keeper.Uint64Bz(order.OrderId) + } + + fiveOrders := []*exchange.Order{ + exchange.NewOrder(14).WithAsk(&exchange.AskOrder{ + MarketId: 8, Seller: s.addr1.String(), Assets: s.coin("14apple"), Price: s.coin("14prune"), + SellerSettlementFlatFee: s.coinP("14fig"), AllowPartial: false, ExternalId: "external-id-5", + }), + exchange.NewOrder(38).WithBid(&exchange.BidOrder{ + MarketId: 6, Buyer: s.addr1.String(), Assets: s.coin("38apple"), Price: s.coin("38prune"), + BuyerSettlementFees: s.coins("38fig"), AllowPartial: true, ExternalId: "external-id-4", + }), + exchange.NewOrder(39).WithBid(&exchange.BidOrder{ + MarketId: 5, Buyer: s.addr1.String(), Assets: s.coin("39apple"), Price: s.coin("39prune"), + BuyerSettlementFees: s.coins("39fig"), AllowPartial: false, ExternalId: "external-id-1", + }), + exchange.NewOrder(71).WithAsk(&exchange.AskOrder{ + MarketId: 5, Seller: s.addr3.String(), Assets: s.coin("71apple"), Price: s.coin("71prune"), + SellerSettlementFlatFee: s.coinP("71fig"), AllowPartial: true, ExternalId: "external-id-3", + }), + exchange.NewOrder(73).WithBid(&exchange.BidOrder{ + MarketId: 5, Buyer: s.addr2.String(), Assets: s.coin("73apple"), Price: s.coin("73prune"), + BuyerSettlementFees: s.coins("73fig"), AllowPartial: false, ExternalId: "external-id-2", + }), + } + fiveOrderSetup := func() { + store := s.getStore() + s.requireSetOrderInStore(store, fiveOrders[2]) + s.requireSetOrderInStore(store, fiveOrders[4]) + s.requireSetOrderInStore(store, fiveOrders[3]) + s.requireSetOrderInStore(store, fiveOrders[1]) + s.requireSetOrderInStore(store, fiveOrders[0]) + } + + tests := []queryTestCase[exchange.QueryGetAllOrdersRequest, exchange.QueryGetAllOrdersResponse]{ + { + name: "bad key in store", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1prune"), + BuyerSettlementFees: s.coins("1fig"), AllowPartial: false, ExternalId: "external-id-1", + })) + + key2, value2, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2prune"), + BuyerSettlementFees: s.coins("2fig"), AllowPartial: false, ExternalId: "external-id-2", + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 2") + store.Set(s.badKey(key2), value2) + + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("3prune"), + SellerSettlementFlatFee: s.coinP("3fig"), AllowPartial: false, ExternalId: "external-id-3", + })) + }, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1prune"), + BuyerSettlementFees: s.coins("1fig"), AllowPartial: false, ExternalId: "external-id-1", + }), + exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("3prune"), + SellerSettlementFlatFee: s.coinP("3fig"), AllowPartial: false, ExternalId: "external-id-3", + }), + }, + Pagination: &query.PageResponse{Total: 2}, + }, + }, + { + name: "bad order in store", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1prune"), + BuyerSettlementFees: s.coins("1fig"), AllowPartial: false, ExternalId: "external-id-1", + })) + + key2, value2, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("2apple"), Price: s.coin("2prune"), + BuyerSettlementFees: s.coins("2fig"), AllowPartial: false, ExternalId: "external-id-2", + })) + s.Require().NoError(err, "GetOrderStoreKeyValue 2") + value2[0] = 9 + store.Set(key2, value2) + + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("3prune"), + SellerSettlementFlatFee: s.coinP("3fig"), AllowPartial: false, ExternalId: "external-id-3", + })) + }, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: []*exchange.Order{ + exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1prune"), + BuyerSettlementFees: s.coins("1fig"), AllowPartial: false, ExternalId: "external-id-1", + }), + exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("3prune"), + SellerSettlementFlatFee: s.coinP("3fig"), AllowPartial: false, ExternalId: "external-id-3", + }), + }, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "both offset and key provided", + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{Offset: 2, Key: makeKey(fiveOrders[0])}}, + expInErr: []string{invalidArgErr, "error iterating all orders", + "invalid request, either offset or key is expected, got both"}, + }, + { + name: "no orders in state", + expResp: &exchange.QueryGetAllOrdersResponse{Pagination: &query.PageResponse{}}, + }, + { + name: "5 orders: get all: nil req", + setup: fiveOrderSetup, + req: nil, + expResp: &exchange.QueryGetAllOrdersResponse{Orders: fiveOrders, Pagination: &query.PageResponse{Total: 5}}, + }, + { + name: "5 orders: get all: empty req", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{}, + expResp: &exchange.QueryGetAllOrdersResponse{Orders: fiveOrders, Pagination: &query.PageResponse{Total: 5}}, + }, + { + name: "5 orders: get all: empty pagination", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{}}, + expResp: &exchange.QueryGetAllOrdersResponse{Orders: fiveOrders, Pagination: &query.PageResponse{Total: 5}}, + }, + { + name: "5 orders: limit 2", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{Limit: 2}}, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: fiveOrders[0:2], + Pagination: &query.PageResponse{NextKey: makeKey(fiveOrders[2])}, + }, + }, + { + name: "5 orders: get second using key", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{Limit: 1, Key: makeKey(fiveOrders[1])}}, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: fiveOrders[1:2], + Pagination: &query.PageResponse{NextKey: makeKey(fiveOrders[2])}, + }, + }, + { + name: "5 orders: get third and fourth using offset", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{Limit: 2, Offset: 2}}, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: fiveOrders[2:4], + Pagination: &query.PageResponse{NextKey: makeKey(fiveOrders[4])}, + }, + }, + { + name: "5 orders: get all: reversed", + setup: fiveOrderSetup, + req: &exchange.QueryGetAllOrdersRequest{Pagination: &query.PageRequest{Reverse: true}}, + expResp: &exchange.QueryGetAllOrdersResponse{ + Orders: reverseSlice(fiveOrders), + Pagination: &query.PageResponse{Total: 5}}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetMarket() { + testDef := queryTestDef[exchange.QueryGetMarketRequest, exchange.QueryGetMarketResponse]{ + queryName: "GetMarket", + query: keeper.NewQueryServer(s.k).GetMarket, + } + + tests := []queryTestCase[exchange.QueryGetMarketRequest, exchange.QueryGetMarketResponse]{ + { + name: "nil request", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "market 0", + req: &exchange.QueryGetMarketRequest{MarketId: 0}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty state", + req: &exchange.QueryGetMarketRequest{MarketId: 1}, + expInErr: []string{invalidArgErr, "market 1 not found"}, + }, + { + name: "market does not exist", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1}) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 2}) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 4}) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 5}) + }, + req: &exchange.QueryGetMarketRequest{MarketId: 3}, + expInErr: []string{invalidArgErr, "market 3 not found"}, + }, + { + name: "market exists", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1}) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 2}) + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "This is the third market. Not the first or second. And fourth is just too far.", + WebsiteUrl: "not actually a websute url for market 3", + IconUri: "https://www.example.com/market/3/icon", + }, + FeeCreateAskFlat: s.coins("10fig,100grape"), + FeeCreateBidFlat: s.coins("20fig,200grape"), + FeeSellerSettlementFlat: s.coins("10pineapple,50prune"), + FeeSellerSettlementRatios: s.ratios("1000pineapple:1pineapple,100prune:1prune"), + FeeBuyerSettlementFlat: s.coins("12pineapple60prune"), + FeeBuyerSettlementRatios: s.ratios("1000pineapple:3pineapple,100prune:3prune"), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + {Address: s.addr2.String(), Permissions: []exchange.Permission{1, 2}}, + {Address: s.addr3.String(), Permissions: []exchange.Permission{3, 4}}, + {Address: s.addr4.String(), Permissions: []exchange.Permission{5, 6}}, + {Address: s.addr5.String(), Permissions: []exchange.Permission{2, 4, 6, 7}}, + }, + ReqAttrCreateAsk: []string{"ask.good.kyc", "*.my.custom"}, + ReqAttrCreateBid: []string{"bid.good.kyc", "*.my.custom"}, + }) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 4}) + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 5}) + }, + req: &exchange.QueryGetMarketRequest{MarketId: 3}, + expResp: &exchange.QueryGetMarketResponse{ + Address: exchange.GetMarketAddress(3).String(), + Market: &exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "This is the third market. Not the first or second. And fourth is just too far.", + WebsiteUrl: "not actually a websute url for market 3", + IconUri: "https://www.example.com/market/3/icon", + }, + FeeCreateAskFlat: s.coins("10fig,100grape"), + FeeCreateBidFlat: s.coins("20fig,200grape"), + FeeSellerSettlementFlat: s.coins("10pineapple,50prune"), + FeeSellerSettlementRatios: s.ratios("1000pineapple:1pineapple,100prune:1prune"), + FeeBuyerSettlementFlat: s.coins("12pineapple60prune"), + FeeBuyerSettlementRatios: s.ratios("1000pineapple:3pineapple,100prune:3prune"), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + {Address: s.addr2.String(), Permissions: []exchange.Permission{1, 2}}, + {Address: s.addr3.String(), Permissions: []exchange.Permission{3, 4}}, + {Address: s.addr4.String(), Permissions: []exchange.Permission{5, 6}}, + {Address: s.addr5.String(), Permissions: []exchange.Permission{2, 4, 6, 7}}, + }, + ReqAttrCreateAsk: []string{"ask.good.kyc", "*.my.custom"}, + ReqAttrCreateBid: []string{"bid.good.kyc", "*.my.custom"}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_GetAllMarkets() { + briefIDStringer := func(brief *exchange.MarketBrief) string { + if brief == nil { + return "" + } + return fmt.Sprintf("%d", brief.MarketId) + } + testDef := queryTestDef[exchange.QueryGetAllMarketsRequest, exchange.QueryGetAllMarketsResponse]{ + queryName: "GetAllMarkets", + query: keeper.NewQueryServer(s.k).GetAllMarkets, + followup: func(expected, actual *exchange.QueryGetAllMarketsResponse) { + assertEqualSlice(s, expected.Markets, actual.Markets, briefIDStringer, "Markets") + s.assertEqualPageResponse(expected.Pagination, actual.Pagination, "Pagination") + }, + } + makeKey := func(market *exchange.Market) []byte { + return keeper.Uint32Bz(market.MarketId) + } + + newMarket := func(marketID uint32) *exchange.Market { + return &exchange.Market{ + MarketId: marketID, + MarketDetails: exchange.MarketDetails{ + Name: fmt.Sprintf("Market %d", marketID), + Description: fmt.Sprintf("This is the description of market %d.", marketID), + WebsiteUrl: fmt.Sprintf("https://example.com/market/%d/info", marketID), + IconUri: fmt.Sprintf("https://example.com/market/%d/icon", marketID), + }, + } + } + fiveMarkets := []*exchange.Market{ + newMarket(6), + newMarket(34), + newMarket(53), + newMarket(81), + newMarket(98), + } + fiveMarketsSetup := func() { + s.requireCreateMarketUnmocked(*fiveMarkets[1]) + s.requireCreateMarketUnmocked(*fiveMarkets[0]) + s.requireCreateMarketUnmocked(*fiveMarkets[3]) + s.requireCreateMarketUnmocked(*fiveMarkets[2]) + s.requireCreateMarketUnmocked(*fiveMarkets[4]) + } + + newBrief := func(marketID uint32) *exchange.MarketBrief { + market := newMarket(marketID) + return &exchange.MarketBrief{ + MarketId: market.MarketId, + MarketAddress: exchange.GetMarketAddress(market.MarketId).String(), + MarketDetails: market.MarketDetails, + } + } + fiveBriefs := make([]*exchange.MarketBrief, len(fiveMarkets)) + for i, market := range fiveMarkets { + fiveBriefs[i] = newBrief(market.MarketId) + } + + tests := []queryTestCase[exchange.QueryGetAllMarketsRequest, exchange.QueryGetAllMarketsResponse]{ + { + name: "both key and offset provided", + req: &exchange.QueryGetAllMarketsRequest{ + Pagination: &query.PageRequest{Key: makeKey(fiveMarkets[1]), Offset: 3}, + }, + expInErr: []string{invalidArgErr, "error iterating all known markets", + "invalid request, either offset or key is expected, got both"}, + }, + { + name: "bad market key", + setup: func() { + s.requireCreateMarketUnmocked(*newMarket(1)) + s.getStore().Set(s.badKey(keeper.MakeKeyKnownMarketID(2)), []byte{}) + s.requireCreateMarketUnmocked(*newMarket(3)) + }, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: []*exchange.MarketBrief{newBrief(1), newBrief(3)}, + Pagination: &query.PageResponse{Total: 2}, + }, + }, + { + name: "market account does not exist", + setup: func() { + s.requireCreateMarketUnmocked(*newMarket(1)) + keeper.StoreMarket(s.getStore(), *newMarket(2)) + s.requireCreateMarketUnmocked(*newMarket(3)) + }, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: []*exchange.MarketBrief{newBrief(1), newBrief(3)}, + Pagination: &query.PageResponse{Total: 3}, + }, + }, + { + name: "no markets in state", + expResp: &exchange.QueryGetAllMarketsResponse{Pagination: &query.PageResponse{Total: 0}}, + }, + { + name: "five markets: nil req", + setup: fiveMarketsSetup, + req: nil, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs, + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "five markets: empty req", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs, + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "five markets: empty pagination", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs, + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "five markets: reversed", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{Reverse: true}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: reverseSlice(fiveBriefs), + Pagination: &query.PageResponse{Total: 5}, + }, + }, + { + name: "five markets: limit 3", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{Limit: 3}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs[0:3], + Pagination: &query.PageResponse{NextKey: makeKey(fiveMarkets[3])}, + }, + }, + { + name: "five markets: limit 3, reversed", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{Limit: 3, Reverse: true}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: reverseSlice(fiveBriefs[2:]), + Pagination: &query.PageResponse{NextKey: makeKey(fiveMarkets[1])}, + }, + }, + { + name: "five markets: just second using key", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{Limit: 1, Key: makeKey(fiveMarkets[1])}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs[1:2], + Pagination: &query.PageResponse{NextKey: makeKey(fiveMarkets[2])}, + }, + }, + { + name: "five markets: just third and fourth using offset", + setup: fiveMarketsSetup, + req: &exchange.QueryGetAllMarketsRequest{Pagination: &query.PageRequest{Limit: 2, Offset: 2}}, + expResp: &exchange.QueryGetAllMarketsResponse{ + Markets: fiveBriefs[2:4], + Pagination: &query.PageResponse{NextKey: makeKey(fiveMarkets[4])}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_Params() { + testDef := queryTestDef[exchange.QueryParamsRequest, exchange.QueryParamsResponse]{ + queryName: "Params", + query: keeper.NewQueryServer(s.k).Params, + } + + tests := []queryTestCase[exchange.QueryParamsRequest, exchange.QueryParamsResponse]{ + { + name: "no params in state, nil req", + setup: func() { + s.k.SetParams(s.ctx, nil) + }, + req: nil, + expResp: &exchange.QueryParamsResponse{Params: exchange.DefaultParams()}, + }, + { + name: "no params in state, empty req", + setup: func() { + s.k.SetParams(s.ctx, nil) + }, + req: &exchange.QueryParamsRequest{}, + expResp: &exchange.QueryParamsResponse{Params: exchange.DefaultParams()}, + }, + { + name: "default params in state, nil req", + setup: func() { + s.k.SetParams(s.ctx, exchange.DefaultParams()) + }, + req: nil, + expResp: &exchange.QueryParamsResponse{Params: exchange.DefaultParams()}, + }, + { + name: "default params in state, empty req", + setup: func() { + s.k.SetParams(s.ctx, exchange.DefaultParams()) + }, + req: nil, + expResp: &exchange.QueryParamsResponse{Params: exchange.DefaultParams()}, + }, + { + name: "just the default split changed", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 987}) + }, + expResp: &exchange.QueryParamsResponse{Params: &exchange.Params{DefaultSplit: 987}}, + }, + { + name: "with denom splits, nil req", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 0, + DenomSplits: []exchange.DenomSplit{ + {Denom: "apple", Split: 500}, + {Denom: "banana", Split: 333}, // mmmmmmmm + {Denom: "cactus", Split: 777}, + }, + }) + }, + expResp: &exchange.QueryParamsResponse{Params: &exchange.Params{ + DefaultSplit: 0, + DenomSplits: []exchange.DenomSplit{ + {Denom: "apple", Split: 500}, + {Denom: "banana", Split: 333}, + {Denom: "cactus", Split: 777}, + }, + }}, + }, + { + name: "with denom splits, empty req", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 1000, + DenomSplits: []exchange.DenomSplit{ + {Denom: "acorn", Split: 600}, + {Denom: "blueberry", Split: 55}, + {Denom: "cherry", Split: 1234}, + {Denom: "date", Split: 1000}, + }, + }) + }, + req: &exchange.QueryParamsRequest{}, + expResp: &exchange.QueryParamsResponse{Params: &exchange.Params{ + DefaultSplit: 1000, + DenomSplits: []exchange.DenomSplit{ + {Denom: "acorn", Split: 600}, + {Denom: "blueberry", Split: 55}, + {Denom: "cherry", Split: 1234}, + {Denom: "date", Split: 1000}, + }, + }}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_ValidateCreateMarket() { + testDef := queryTestDef[exchange.QueryValidateCreateMarketRequest, exchange.QueryValidateCreateMarketResponse]{ + queryName: "ValidateCreateMarket", + query: keeper.NewQueryServer(s.k).ValidateCreateMarket, + } + + tests := []queryTestCase[exchange.QueryValidateCreateMarketRequest, exchange.QueryValidateCreateMarketResponse]{ + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty req", + req: &exchange.QueryValidateCreateMarketRequest{}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "invalid market", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: strings.Repeat("s", exchange.MaxName+1)}, + }, + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + Error: fmt.Sprintf("name length %d exceeds maximum length of %d", + exchange.MaxName+1, exchange.MaxName), + }, + }, + { + name: "no authority", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: "", + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + Error: "invalid authority: empty address string is not allowed", + }, + }, + { + name: "bad authority", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: "bad", + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + Error: "invalid authority: decoding bech32 failed: invalid bech32 string length 3", + }, + }, + { + name: "wrong authority", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: s.addr1.String(), + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + Error: "expected \"" + s.k.GetAuthority() + "\" got \"" + s.addr1.String() + "\": " + + "expected gov account as only signer for proposal message", + }, + }, + { + name: "market already exists", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1}) + }, + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{MarketId: 1}, + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + Error: "market id 1 account " + exchange.GetMarketAddress(1).String() + " already exists", + }, + }, + { + name: "problems with market definition", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + ReqAttrCreateAsk: []string{" ask .bb.cc"}, + ReqAttrCreateBid: []string{" bid .bb.cc"}, + }, + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "create ask required attribute \" ask .bb.cc\" is not normalized, expected \"ask.bb.cc\"", + "create bid required attribute \" bid .bb.cc\" is not normalized, expected \"bid.bb.cc\"", + ), + }, + }, + { + name: "all good", + req: &exchange.QueryValidateCreateMarketRequest{CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + ReqAttrCreateAsk: []string{"ask.bb.cc"}, + ReqAttrCreateBid: []string{"bid.bb.cc"}, + }, + }}, + expResp: &exchange.QueryValidateCreateMarketResponse{GovPropWillPass: true, Error: ""}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_ValidateMarket() { + testDef := queryTestDef[exchange.QueryValidateMarketRequest, exchange.QueryValidateMarketResponse]{ + queryName: "ValidateMarket", + query: keeper.NewQueryServer(s.k).ValidateMarket, + } + + tests := []queryTestCase[exchange.QueryValidateMarketRequest, exchange.QueryValidateMarketResponse]{ + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "market 0", + req: &exchange.QueryValidateMarketRequest{MarketId: 0}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "market does not exist", + req: &exchange.QueryValidateMarketRequest{MarketId: 66}, + expResp: &exchange.QueryValidateMarketResponse{Error: "market 66 does not exist"}, + }, + { + name: "bad ratios", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + FeeSellerSettlementRatios: s.ratios("100peach:1peach,100plum:3plum"), + FeeBuyerSettlementRatios: s.ratios("100plum:1plum,100prune:7prune"), + }) + }, + req: &exchange.QueryValidateMarketRequest{MarketId: 2}, + expResp: &exchange.QueryValidateMarketResponse{Error: s.joinErrs( + "seller settlement fee ratios have price denom \"peach\" but there are no "+ + "buyer settlement fee ratios with that price denom", + "buyer settlement fee ratios have price denom \"prune\" but there is not a "+ + "seller settlement fee ratio with that price denom", + )}, + }, + { + name: "all good", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + FeeSellerSettlementRatios: s.ratios("100peach:1peach,100plum:3plum,100prune:7prune"), + FeeBuyerSettlementRatios: s.ratios("100peach:3peach,100plum:7plum,100prune:1prune"), + }) + }, + req: &exchange.QueryValidateMarketRequest{MarketId: 2}, + expResp: &exchange.QueryValidateMarketResponse{Error: ""}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestQueryServer_ValidateManageFees() { + testDef := queryTestDef[exchange.QueryValidateManageFeesRequest, exchange.QueryValidateManageFeesResponse]{ + queryName: "ValidateManageFees", + query: keeper.NewQueryServer(s.k).ValidateManageFees, + } + + tests := []queryTestCase[exchange.QueryValidateManageFeesRequest, exchange.QueryValidateManageFeesResponse]{ + { + name: "nil req", + req: nil, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "empty req", + req: &exchange.QueryValidateManageFeesRequest{}, + expInErr: []string{invalidArgErr, "empty request"}, + }, + { + name: "invalid msg", + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: "", MarketId: 1, + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + Error: s.joinErrs( + "invalid authority: empty address string is not allowed", + "no updates", + ), + }, + }, + { + name: "wrong authority", + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.addr1.String(), MarketId: 1, + AddFeeCreateAskFlat: s.coins("100plum"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + Error: "expected \"" + s.k.GetAuthority() + "\" got \"" + s.addr1.String() + "\": " + + "expected gov account as only signer for proposal message", + }, + }, + { + name: "market does not exist", + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 1, + AddFeeCreateAskFlat: s.coins("100plum"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + Error: "market 1 does not exist", + }, + }, + { + name: "add/rem create-ask errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeCreateAskFlat: s.coins("100peach"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeCreateAskFlat: s.coins("100plum"), + AddFeeCreateAskFlat: s.coins("90peach"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove create-ask flat fee \"100plum\": no such fee exists", + "cannot add create-ask flat fee \"90peach\": fee with that denom already exists", + ), + }, + }, + { + name: "add/rem create-bid errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeCreateBidFlat: s.coins("100apple"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeCreateBidFlat: s.coins("100acorn"), + AddFeeCreateBidFlat: s.coins("90apple"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove create-bid flat fee \"100acorn\": no such fee exists", + "cannot add create-bid flat fee \"90apple\": fee with that denom already exists", + ), + }, + }, + { + name: "add/rem seller flat errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeSellerSettlementFlat: s.coins("100cherry"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeSellerSettlementFlat: s.coins("100cactus"), + AddFeeSellerSettlementFlat: s.coins("90cherry"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove seller settlement flat fee \"100cactus\": no such fee exists", + "cannot add seller settlement flat fee \"90cherry\": fee with that denom already exists", + ), + }, + }, + { + name: "add/rem seller ratio errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeSellerSettlementRatios: s.ratios("100pear:1pear"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeSellerSettlementRatios: s.ratios("100prune:1prune"), + AddFeeSellerSettlementRatios: s.ratios("90pear:1pear"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove seller settlement ratio fee \"100prune:1prune\": no such ratio exists", + "cannot add seller settlement ratio fee \"90pear:1pear\": ratio with those denoms already exists", + ), + }, + }, + { + name: "add/rem buyer flat errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeBuyerSettlementFlat: s.coins("100date"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeBuyerSettlementFlat: s.coins("100durian"), + AddFeeBuyerSettlementFlat: s.coins("90date"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove buyer settlement flat fee \"100durian\": no such fee exists", + "cannot add buyer settlement flat fee \"90date\": fee with that denom already exists", + ), + }, + }, + { + name: "add/rem buyer ratio errors", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeBuyerSettlementRatios: s.ratios("100banana:1banana"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeBuyerSettlementRatios: s.ratios("100blueberry:1blueberry"), + AddFeeBuyerSettlementRatios: s.ratios("90banana:1banana"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove buyer settlement ratio fee \"100blueberry:1blueberry\": no such ratio exists", + "cannot add buyer settlement ratio fee \"90banana:1banana\": ratio with those denoms already exists", + ), + }, + }, + { + name: "seller ratio problems after add", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 4, + FeeSellerSettlementRatios: s.ratios("100banana:1banana"), + FeeBuyerSettlementRatios: s.ratios("100banana:3banana"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 4, + AddFeeSellerSettlementRatios: s.ratios("90apple:1apple"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: "seller settlement fee ratios have price denom \"apple\" but there are no " + + "buyer settlement fee ratios with that price denom", + }, + }, + { + name: "seller ratio problems after remove", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 4, + FeeSellerSettlementRatios: s.ratios("100banana:1banana,90apple:1apple"), + FeeBuyerSettlementRatios: s.ratios("100banana:3banana,90apple:7apple"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 4, + RemoveFeeSellerSettlementRatios: s.ratios("90apple:1apple"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: "buyer settlement fee ratios have price denom \"apple\" but there is not a " + + "seller settlement fee ratio with that price denom", + }, + }, + { + name: "buyer ratio problems after add", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 4, + FeeSellerSettlementRatios: s.ratios("100banana:1banana"), + FeeBuyerSettlementRatios: s.ratios("100banana:3banana"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 4, + AddFeeBuyerSettlementRatios: s.ratios("90apple:7apple"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: "buyer settlement fee ratios have price denom \"apple\" but there is not a " + + "seller settlement fee ratio with that price denom", + }, + }, + { + name: "buyer ratio problems after remove", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 4, + FeeSellerSettlementRatios: s.ratios("100banana:1banana,90apple:1apple"), + FeeBuyerSettlementRatios: s.ratios("100banana:3banana,90apple:7apple"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 4, + RemoveFeeBuyerSettlementRatios: s.ratios("90apple:7apple"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: "seller settlement fee ratios have price denom \"apple\" but there are no " + + "buyer settlement fee ratios with that price denom", + }, + }, + { + name: "all the problems", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeCreateAskFlat: s.coins("100peach"), + FeeCreateBidFlat: s.coins("100apple"), + FeeSellerSettlementFlat: s.coins("100cherry"), + FeeSellerSettlementRatios: s.ratios("100pear:1pear"), + FeeBuyerSettlementFlat: s.coins("100date"), + FeeBuyerSettlementRatios: s.ratios("100banana:1banana"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeCreateAskFlat: s.coins("100plum"), + AddFeeCreateAskFlat: s.coins("90peach"), + RemoveFeeCreateBidFlat: s.coins("100acorn"), + AddFeeCreateBidFlat: s.coins("90apple"), + RemoveFeeSellerSettlementFlat: s.coins("100cactus"), + AddFeeSellerSettlementFlat: s.coins("90cherry"), + RemoveFeeSellerSettlementRatios: s.ratios("100prune:1prune"), + AddFeeSellerSettlementRatios: s.ratios("90pear:1pear"), + RemoveFeeBuyerSettlementFlat: s.coins("100durian"), + AddFeeBuyerSettlementFlat: s.coins("90date"), + RemoveFeeBuyerSettlementRatios: s.ratios("100blueberry:1blueberry"), + AddFeeBuyerSettlementRatios: s.ratios("90banana:1banana"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{ + GovPropWillPass: true, + Error: s.joinErrs( + "cannot remove create-ask flat fee \"100plum\": no such fee exists", + "cannot add create-ask flat fee \"90peach\": fee with that denom already exists", + "cannot remove create-bid flat fee \"100acorn\": no such fee exists", + "cannot add create-bid flat fee \"90apple\": fee with that denom already exists", + "cannot remove seller settlement flat fee \"100cactus\": no such fee exists", + "cannot add seller settlement flat fee \"90cherry\": fee with that denom already exists", + "cannot remove seller settlement ratio fee \"100prune:1prune\": no such ratio exists", + "cannot add seller settlement ratio fee \"90pear:1pear\": ratio with those denoms already exists", + "cannot remove buyer settlement flat fee \"100durian\": no such fee exists", + "cannot add buyer settlement flat fee \"90date\": fee with that denom already exists", + "cannot remove buyer settlement ratio fee \"100blueberry:1blueberry\": no such ratio exists", + "cannot add buyer settlement ratio fee \"90banana:1banana\": ratio with those denoms already exists", + "seller settlement fee ratios have price denom \"pear\" but there are no "+ + "buyer settlement fee ratios with that price denom", + "buyer settlement fee ratios have price denom \"banana\" but there is not a "+ + "seller settlement fee ratio with that price denom", + ), + }, + }, + { + name: "all good", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 7, + FeeCreateAskFlat: s.coins("100peach"), + FeeCreateBidFlat: s.coins("100apple"), + FeeSellerSettlementFlat: s.coins("100cherry"), + FeeSellerSettlementRatios: s.ratios("100pear:1pear"), + FeeBuyerSettlementFlat: s.coins("100date"), + FeeBuyerSettlementRatios: s.ratios("100banana:1banana"), + }) + }, + req: &exchange.QueryValidateManageFeesRequest{ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), MarketId: 7, + RemoveFeeCreateAskFlat: s.coins("100peach"), + AddFeeCreateAskFlat: s.coins("90peach"), + RemoveFeeCreateBidFlat: s.coins("100apple"), + AddFeeCreateBidFlat: s.coins("90apple"), + RemoveFeeSellerSettlementFlat: s.coins("100cherry"), + AddFeeSellerSettlementFlat: s.coins("90cherry"), + RemoveFeeSellerSettlementRatios: s.ratios("100pear:1pear"), + AddFeeSellerSettlementRatios: s.ratios("90pear:1pear,100banana:1banana"), + RemoveFeeBuyerSettlementFlat: s.coins("100date"), + AddFeeBuyerSettlementFlat: s.coins("90date"), + RemoveFeeBuyerSettlementRatios: s.ratios("100banana:1banana"), + AddFeeBuyerSettlementRatios: s.ratios("90banana:1banana,100pear:1pear"), + }}, + expResp: &exchange.QueryValidateManageFeesResponse{GovPropWillPass: true, Error: ""}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runQueryTestCase(s, testDef, tc) + }) + } +} diff --git a/x/exchange/keeper/keeper_test.go b/x/exchange/keeper/keeper_test.go index 17c27142ff..cd8ab27666 100644 --- a/x/exchange/keeper/keeper_test.go +++ b/x/exchange/keeper/keeper_test.go @@ -1,193 +1,17 @@ package keeper_test import ( - "context" "fmt" "strings" - "testing" - "github.com/stretchr/testify/suite" - - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/cosmos/cosmos-sdk/x/bank/testutil" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/provenance-io/provenance/app" - "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" - "github.com/provenance-io/provenance/x/exchange/keeper" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) -type TestSuite struct { - suite.Suite - - app *app.App - ctx sdk.Context - stdlibCtx context.Context - - k keeper.Keeper - acctKeeper exchange.AccountKeeper - attrKeeper exchange.AttributeKeeper - bankKeeper exchange.BankKeeper - holdKeeper exchange.HoldKeeper - - bondDenom string - initBal sdk.Coins - initAmount int64 - - addr1 sdk.AccAddress - addr2 sdk.AccAddress - addr3 sdk.AccAddress - addr4 sdk.AccAddress - addr5 sdk.AccAddress - - marketAddr1 sdk.AccAddress - marketAddr2 sdk.AccAddress - marketAddr3 sdk.AccAddress - - feeCollector string -} - -func (s *TestSuite) SetupTest() { - s.app = app.Setup(s.T()) - s.ctx = s.app.BaseApp.NewContext(false, tmproto.Header{}) - s.stdlibCtx = sdk.WrapSDKContext(s.ctx) - s.k = s.app.ExchangeKeeper - s.acctKeeper = s.app.AccountKeeper - s.attrKeeper = s.app.AttributeKeeper - s.bankKeeper = s.app.BankKeeper - s.holdKeeper = s.app.HoldKeeper - - s.bondDenom = s.app.StakingKeeper.BondDenom(s.ctx) - s.initAmount = 1_000_000_000 - s.initBal = sdk.NewCoins(sdk.NewCoin(s.bondDenom, sdk.NewInt(s.initAmount))) - - addrs := app.AddTestAddrsIncremental(s.app, s.ctx, 5, sdk.NewInt(s.initAmount)) - s.addr1 = addrs[0] - s.addr2 = addrs[1] - s.addr3 = addrs[2] - s.addr4 = addrs[3] - s.addr5 = addrs[4] - - s.marketAddr1 = exchange.GetMarketAddress(1) - s.marketAddr2 = exchange.GetMarketAddress(2) - s.marketAddr3 = exchange.GetMarketAddress(3) - - s.feeCollector = s.k.GetFeeCollectorName() -} - -func TestKeeperTestSuite(t *testing.T) { - suite.Run(t, new(TestSuite)) -} - -// coins creates an sdk.Coins from a string, requiring it to work. -func (s *TestSuite) coins(coins string) sdk.Coins { - s.T().Helper() - rv, err := sdk.ParseCoinsNormalized(coins) - s.Require().NoError(err, "ParseCoinsNormalized(%q)", coins) - return rv -} - -// coin creates a new coin without doing any validation on it. -func (s *TestSuite) coin(amount int64, denom string) sdk.Coin { - return sdk.Coin{ - Amount: s.int(amount), - Denom: denom, - } -} - -// int is a shorter way to call sdkmath.NewInt. -func (s *TestSuite) int(amount int64) sdkmath.Int { - return sdkmath.NewInt(amount) -} - -// intStr creates an sdkmath.Int from a string, requiring it to work. -func (s *TestSuite) intStr(amount string) sdkmath.Int { - s.T().Helper() - rv, ok := sdkmath.NewIntFromString(amount) - s.Require().True(ok, "NewIntFromString(%q) ok bool", amount) - return rv -} - -// getAddrName returns the name of the variable in this TestSuite holding the provided address. -func (s *TestSuite) getAddrName(addr sdk.AccAddress) string { - switch string(addr) { - case string(s.addr1): - return "addr1" - case string(s.addr2): - return "addr2" - case string(s.addr3): - return "addr3" - case string(s.addr4): - return "addr4" - case string(s.addr5): - return "addr5" - case string(s.marketAddr1): - return "marketAddr1" - case string(s.marketAddr2): - return "marketAddr2" - case string(s.marketAddr3): - return "marketAddr3" - default: - return addr.String() - } -} - -// getAddrStrName returns the name of the variable in this TestSuite holding the provided address. -func (s *TestSuite) getAddrStrName(addrStr string) string { - addr, err := sdk.AccAddressFromBech32(addrStr) - if err != nil { - return addrStr - } - return s.getAddrName(addr) -} - -// getStore gets the exchange store. -func (s *TestSuite) getStore() sdk.KVStore { - return s.k.GetStore(s.ctx) -} - -// clearExchangeState deletes everything from the exchange state store. -func (s *TestSuite) clearExchangeState() { - keeper.DeleteAll(s.getStore(), nil) -} - -// stateEntryString converts the provided key and value into a ""="" string. -func (s *TestSuite) stateEntryString(key, value []byte) string { - return fmt.Sprintf("%q=%q", key, value) -} - -// dumpHoldState creates a string for each entry in the hold state store. -// Each entry has the format `""=""`. -func (s *TestSuite) dumpHoldState() []string { - var rv []string - keeper.Iterate(s.getStore(), nil, func(key, value []byte) bool { - rv = append(rv, s.stateEntryString(key, value)) - return false - }) - return rv -} - -// requireFundAccount calls testutil.FundAccount, making sure it doesn't panic or return an error. -func (s *TestSuite) requireFundAccount(addr sdk.AccAddress, coins string) { - assertions.RequireNotPanicsNoErrorf(s.T(), func() error { - return testutil.FundAccount(s.app.BankKeeper, s.ctx, addr, s.coins(coins)) - }, "FundAccount(%s, %q)", s.getAddrName(addr), coins) -} - -// assertErrorValue is a wrapper for assertions.AssertErrorValue for this TestSuite. -func (s *TestSuite) assertErrorValue(theError error, expected string, msgAndArgs ...interface{}) bool { - return assertions.AssertErrorValue(s.T(), theError, expected, msgAndArgs...) -} - -// assertErrorContents is a wrapper for assertions.AssertErrorContents for this TestSuite. -func (s *TestSuite) assertErrorContents(theError error, contains []string, msgAndArgs ...interface{}) bool { - return assertions.AssertErrorContents(s.T(), theError, contains, msgAndArgs...) -} - func (s *TestSuite) TestKeeper_GetAuthority() { expected := authtypes.NewModuleAddress(govtypes.ModuleName).String() var actual string @@ -372,12 +196,12 @@ func (s *TestSuite) TestKeeper_DoTransfer() { tc.bk = NewMockBankKeeper() } expCalls := BankCalls{ - SendCoinsCalls: tc.expSends, - SendCoinsFromAccountToModuleCalls: nil, - InputOutputCoinsCalls: nil, + SendCoins: tc.expSends, + SendCoinsFromAccountToModule: nil, + InputOutputCoins: nil, } if tc.expIO { - expCalls.InputOutputCoinsCalls = append(expCalls.InputOutputCoinsCalls, &InputOutputCoinsArgs{ + expCalls.InputOutputCoins = append(expCalls.InputOutputCoins, &InputOutputCoinsArgs{ ctxHasQuarantineBypass: true, inputs: tc.inputs, outputs: tc.outputs, @@ -535,7 +359,7 @@ func (s *TestSuite) TestKeeper_CollectFee() { feeAmt: s.coins("750apple"), expErr: "error transferring 750apple from " + s.addr1.String() + " to market 1: test error F from SendCoins", expCalls: BankCalls{ - SendCoinsCalls: []*SendCoinsArgs{ + SendCoins: []*SendCoinsArgs{ {ctxHasQuarantineBypass: false, fromAddr: s.addr1, toAddr: s.marketAddr1, amt: s.coins("750apple")}, }, }, @@ -548,10 +372,10 @@ func (s *TestSuite) TestKeeper_CollectFee() { feeAmt: s.coins("750apple"), expErr: "error collecting exchange fee 19apple (based off 750apple) from market 2: test error U from SendCoinsFromAccountToModule", expCalls: BankCalls{ - SendCoinsCalls: []*SendCoinsArgs{ + SendCoins: []*SendCoinsArgs{ {fromAddr: s.addr4, toAddr: s.marketAddr2, amt: s.coins("750apple")}, }, - SendCoinsFromAccountToModuleCalls: []*SendCoinsFromAccountToModuleArgs{ + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ {senderAddr: s.marketAddr2, recipientModule: s.feeCollector, amt: s.coins("19apple")}, }, }, @@ -564,7 +388,7 @@ func (s *TestSuite) TestKeeper_CollectFee() { feeAmt: s.coins("1000000apple,5000000fig"), expErr: "", expCalls: BankCalls{ - SendCoinsCalls: []*SendCoinsArgs{ + SendCoins: []*SendCoinsArgs{ {fromAddr: s.addr2, toAddr: s.marketAddr3, amt: s.coins("1000000apple,5000000fig")}, }, }, @@ -575,10 +399,10 @@ func (s *TestSuite) TestKeeper_CollectFee() { payer: s.addr3, feeAmt: s.coins("1005apple,5000fig,999999zucchini"), expCalls: BankCalls{ - SendCoinsCalls: []*SendCoinsArgs{ + SendCoins: []*SendCoinsArgs{ {fromAddr: s.addr3, toAddr: s.marketAddr1, amt: s.coins("1005apple,5000fig,999999zucchini")}, }, - SendCoinsFromAccountToModuleCalls: []*SendCoinsFromAccountToModuleArgs{ + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ {senderAddr: s.marketAddr1, recipientModule: s.feeCollector, amt: s.coins("26apple,500fig")}, }, }, @@ -653,10 +477,10 @@ func (s *TestSuite) TestKeeper_CollectFees() { inputs: []banktypes.Input{{Address: s.addr1.String(), Coins: s.coins("1000apple")}}, expErr: "", expCalls: BankCalls{ - SendCoinsCalls: []*SendCoinsArgs{ + SendCoins: []*SendCoinsArgs{ {fromAddr: s.addr1, toAddr: s.marketAddr2, amt: s.coins("1000apple")}, }, - SendCoinsFromAccountToModuleCalls: []*SendCoinsFromAccountToModuleArgs{ + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ {senderAddr: s.marketAddr2, recipientModule: s.feeCollector, amt: s.coins("25apple")}, }, }, @@ -683,7 +507,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { }, expErr: "error collecting fees for market 1: test error Z from InputOutputCoins", expCalls: BankCalls{ - InputOutputCoinsCalls: []*InputOutputCoinsArgs{ + InputOutputCoins: []*InputOutputCoinsArgs{ { inputs: []banktypes.Input{ {Address: s.addr1.String(), Coins: s.coins("10apple,1fig,1zucchini")}, @@ -708,7 +532,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { }, expErr: "error collecting exchange fee 25apple,301fig (based off 1000apple,3001fig,5010zucchini) from market 1: test error L from SendCoinsFromAccountToModule", expCalls: BankCalls{ - InputOutputCoinsCalls: []*InputOutputCoinsArgs{ + InputOutputCoins: []*InputOutputCoinsArgs{ { inputs: []banktypes.Input{ {Address: s.addr1.String(), Coins: s.coins("1000apple,1fig,10zucchini")}, @@ -720,7 +544,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { }, }, }, - SendCoinsFromAccountToModuleCalls: []*SendCoinsFromAccountToModuleArgs{ + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ {senderAddr: s.marketAddr1, recipientModule: s.feeCollector, amt: s.coins("25apple,301fig")}, }, }, @@ -735,7 +559,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { {Address: s.addr5.String(), Coins: s.coins("5000zucchini")}, }, expCalls: BankCalls{ - InputOutputCoinsCalls: []*InputOutputCoinsArgs{ + InputOutputCoins: []*InputOutputCoinsArgs{ { inputs: []banktypes.Input{ {Address: s.addr1.String(), Coins: s.coins("1000apple,1fig,10zucchini")}, @@ -758,7 +582,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { {Address: s.addr5.String(), Coins: s.coins("5000zucchini")}, }, expCalls: BankCalls{ - InputOutputCoinsCalls: []*InputOutputCoinsArgs{ + InputOutputCoins: []*InputOutputCoinsArgs{ { inputs: []banktypes.Input{ {Address: s.addr1.String(), Coins: s.coins("1000apple,1fig,10zucchini")}, @@ -770,7 +594,7 @@ func (s *TestSuite) TestKeeper_CollectFees() { }, }, }, - SendCoinsFromAccountToModuleCalls: []*SendCoinsFromAccountToModuleArgs{ + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("25apple,301fig")}, }, }, diff --git a/x/exchange/keeper/market.go b/x/exchange/keeper/market.go index 5bf0c64868..36c1099c43 100644 --- a/x/exchange/keeper/market.go +++ b/x/exchange/keeper/market.go @@ -702,7 +702,8 @@ func (k Keeper) UpdateFees(ctx sdk.Context, msg *exchange.MsgGovManageFeesReques k.emitEvent(ctx, exchange.NewEventMarketFeesUpdated(msg.MarketId)) } -// isMarketActive returns true if the provided market is accepting orders. +// isMarketActive returns true if the provided market's inactive flag does not exist. +// See also isMarketKnown. func isMarketActive(store sdk.KVStore, marketID uint32) bool { key := MakeKeyMarketInactive(marketID) return !store.Has(key) @@ -734,14 +735,23 @@ func setUserSettlementAllowed(store sdk.KVStore, marketID uint32, allowed bool) } } -// IsMarketActive returns true if the provided market is accepting orders. +// IsMarketKnown returns true if the provided market id is a known market's id. +func (k Keeper) IsMarketKnown(ctx sdk.Context, marketID uint32) bool { + return isMarketKnown(k.getStore(ctx), marketID) +} + +// IsMarketActive returns true if the provided market is active. func (k Keeper) IsMarketActive(ctx sdk.Context, marketID uint32) bool { - return isMarketActive(k.getStore(ctx), marketID) + store := k.getStore(ctx) + if !isMarketActive(store, marketID) { + return false + } + return isMarketKnown(store, marketID) } // UpdateMarketActive updates the active flag for a market. // An error is returned if the setting is already what is provided. -func (k Keeper) UpdateMarketActive(ctx sdk.Context, marketID uint32, active bool, updatedBy sdk.AccAddress) error { +func (k Keeper) UpdateMarketActive(ctx sdk.Context, marketID uint32, active bool, updatedBy string) error { store := k.getStore(ctx) current := isMarketActive(store, marketID) if current == active { @@ -759,7 +769,7 @@ func (k Keeper) IsUserSettlementAllowed(ctx sdk.Context, marketID uint32) bool { // UpdateUserSettlementAllowed updates the allow-user-settlement flag for a market. // An error is returned if the setting is already what is provided. -func (k Keeper) UpdateUserSettlementAllowed(ctx sdk.Context, marketID uint32, allow bool, updatedBy sdk.AccAddress) error { +func (k Keeper) UpdateUserSettlementAllowed(ctx sdk.Context, marketID uint32, allow bool, updatedBy string) error { store := k.getStore(ctx) current := isUserSettlementAllowed(store, marketID) if current == allow { @@ -817,15 +827,15 @@ func revokeAllMarketPermissions(store sdk.KVStore, marketID uint32) { // getAccessGrants gets all the access grants for a market. func getAccessGrants(store sdk.KVStore, marketID uint32) []exchange.AccessGrant { var rv []exchange.AccessGrant - var lastAG exchange.AccessGrant iterate(store, GetKeyPrefixMarketPermissions(marketID), func(key, _ []byte) bool { addr, perm, err := ParseKeySuffixMarketPermissions(key) if err == nil { - if addr.String() != lastAG.Address { - lastAG = exchange.AccessGrant{Address: addr.String()} - rv = append(rv, lastAG) + last := len(rv) - 1 + if last < 0 || addr.String() != rv[last].Address { + rv = append(rv, exchange.AccessGrant{Address: addr.String()}) + last++ } - lastAG.Permissions = append(lastAG.Permissions, perm) + rv[last].Permissions = append(rv[last].Permissions, perm) } return false }) @@ -908,7 +918,6 @@ func (k Keeper) GetAccessGrants(ctx sdk.Context, marketID uint32) []exchange.Acc // UpdatePermissions updates users permissions in the store using the provided changes. // The caller is responsible for making sure this update should be allowed (e.g. by calling CanManagePermissions first). func (k Keeper) UpdatePermissions(ctx sdk.Context, msg *exchange.MsgMarketManagePermissionsRequest) error { - admin := sdk.MustAccAddressFromBech32(msg.Admin) marketID := msg.MarketId store := k.getStore(ctx) var errs []error @@ -952,7 +961,7 @@ func (k Keeper) UpdatePermissions(ctx sdk.Context, msg *exchange.MsgMarketManage return errors.Join(errs...) } - k.emitEvent(ctx, exchange.NewEventMarketPermissionsUpdated(marketID, admin)) + k.emitEvent(ctx, exchange.NewEventMarketPermissionsUpdated(marketID, msg.Admin)) return nil } @@ -1026,7 +1035,7 @@ func setReqAttrsAsk(store sdk.KVStore, marketID uint32, reqAttrs []string) { // the provided entries to the existing entries. // It is assumed that the attributes have been normalized prior to calling this. func updateReqAttrsAsk(store sdk.KVStore, marketID uint32, toRemove, toAdd []string) error { - return updateReqAttrs(store, marketID, toRemove, toAdd, "create ask", MakeKeyMarketReqAttrAsk) + return updateReqAttrs(store, marketID, toRemove, toAdd, "create-ask", MakeKeyMarketReqAttrAsk) } // getReqAttrsBid gets the attributes required to create a bid order. @@ -1043,7 +1052,7 @@ func setReqAttrsBid(store sdk.KVStore, marketID uint32, reqAttrs []string) { // the provided entries to the existing entries. // It is assumed that the attributes have been normalized prior to calling this. func updateReqAttrsBid(store sdk.KVStore, marketID uint32, toRemove, toAdd []string) error { - return updateReqAttrs(store, marketID, toRemove, toAdd, "create bid", MakeKeyMarketReqAttrBid) + return updateReqAttrs(store, marketID, toRemove, toAdd, "create-bid", MakeKeyMarketReqAttrBid) } // acctHasReqAttrs returns true if either reqAttrs is empty or the provide address has all of them on their account. @@ -1088,8 +1097,6 @@ func (k Keeper) CanCreateBid(ctx sdk.Context, marketID uint32, addr sdk.AccAddre // UpdateReqAttrs updates the required attributes in the store using the provided changes. // The caller is responsible for making sure this update should be allowed (e.g. by calling CanManageReqAttrs first). func (k Keeper) UpdateReqAttrs(ctx sdk.Context, msg *exchange.MsgMarketManageReqAttrsRequest) error { - admin := sdk.MustAccAddressFromBech32(msg.Admin) - var errs []error // We don't care if the attributes to remove are valid so that we // can remove entries that are somehow now invalid. @@ -1120,7 +1127,7 @@ func (k Keeper) UpdateReqAttrs(ctx sdk.Context, msg *exchange.MsgMarketManageReq return errors.Join(errs...) } - k.emitEvent(ctx, exchange.NewEventMarketReqAttrUpdated(marketID, admin)) + k.emitEvent(ctx, exchange.NewEventMarketReqAttrUpdated(marketID, msg.Admin)) return nil } @@ -1155,7 +1162,7 @@ func (k Keeper) GetMarketDetails(ctx sdk.Context, marketID uint32) *exchange.Mar // UpdateMarketDetails updates a market's details. It returns an error if the market account // isn't found or if there aren't any changes provided. -func (k Keeper) UpdateMarketDetails(ctx sdk.Context, marketID uint32, marketDetails exchange.MarketDetails, updatedBy sdk.AccAddress) error { +func (k Keeper) UpdateMarketDetails(ctx sdk.Context, marketID uint32, marketDetails exchange.MarketDetails, updatedBy string) error { if err := marketDetails.Validate(); err != nil { return err } @@ -1224,7 +1231,7 @@ func (k Keeper) initMarket(ctx sdk.Context, store sdk.KVStore, market exchange.M // CreateMarket saves a new market to the store with all the info provided. // If the marketId is zero, the next available one will be used. func (k Keeper) CreateMarket(ctx sdk.Context, market exchange.Market) (uint32, error) { - // Note: The Market is passed in by value, so any alterations to it here will be lost upon return. + // Note: The Market is passed in by value, so any alterations directly to it here will be lost upon return. var errAsk, errBid error market.ReqAttrCreateAsk, errAsk = exchange.NormalizeReqAttrs(market.ReqAttrCreateAsk) market.ReqAttrCreateBid, errBid = exchange.NormalizeReqAttrs(market.ReqAttrCreateBid) @@ -1311,7 +1318,7 @@ func (k Keeper) GetMarketBrief(ctx sdk.Context, marketID uint32) *exchange.Marke // WithdrawMarketFunds transfers funds from a market account to another account. // The caller is responsible for making sure this withdrawal should be allowed (e.g. by calling CanWithdrawMarketFunds first). -func (k Keeper) WithdrawMarketFunds(ctx sdk.Context, marketID uint32, toAddr sdk.AccAddress, amount sdk.Coins, withdrawnBy sdk.AccAddress) error { +func (k Keeper) WithdrawMarketFunds(ctx sdk.Context, marketID uint32, toAddr sdk.AccAddress, amount sdk.Coins, withdrawnBy string) error { marketAddr := exchange.GetMarketAddress(marketID) err := k.bankKeeper.SendCoins(ctx, marketAddr, toAddr, amount) if err != nil { diff --git a/x/exchange/keeper/market_test.go b/x/exchange/keeper/market_test.go index c71d155bf3..df2417161a 100644 --- a/x/exchange/keeper/market_test.go +++ b/x/exchange/keeper/market_test.go @@ -1,89 +1,6743 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateKnownMarketIDs() +import ( + "fmt" + "strings" -// TODO[1658]: func (s *TestSuite) TestKeeper_GetCreateAskFlatFees() + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" -// TODO[1658]: func (s *TestSuite) TestKeeper_GetCreateBidFlatFees() + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) -// TODO[1658]: func (s *TestSuite) TestKeeper_GetSellerSettlementFlatFees() +func (s *TestSuite) TestKeeper_IterateKnownMarketIDs() { + var marketIDs []uint32 + stopAfter := func(n int) func(marketID uint32) bool { + return func(marketID uint32) bool { + marketIDs = append(marketIDs, marketID) + return len(marketIDs) >= n + } + } + getAll := func() func(marketID uint32) bool { + return func(marketID uint32) bool { + marketIDs = append(marketIDs, marketID) + return false + } + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetSellerSettlementRatios() + tests := []struct { + name string + setup func() + cb func(marketID uint32) bool + expMarketIDs []uint32 + }{ + { + name: "no known market ids", + setup: nil, + cb: getAll(), + expMarketIDs: nil, + }, + { + name: "one known market id", + setup: func() { + keeper.SetMarketKnown(s.getStore(), 88) + }, + cb: getAll(), + expMarketIDs: []uint32{88}, + }, + { + name: "three market ids: get all", + setup: func() { + store := s.getStore() + keeper.SetMarketKnown(store, 88) + keeper.SetMarketKnown(store, 3) + keeper.SetMarketKnown(store, 50) + }, + cb: getAll(), + expMarketIDs: []uint32{3, 50, 88}, + }, + { + name: "three market ids: get one", + setup: func() { + store := s.getStore() + keeper.SetMarketKnown(store, 88) + keeper.SetMarketKnown(store, 3) + keeper.SetMarketKnown(store, 50) + }, + cb: stopAfter(1), + expMarketIDs: []uint32{3}, + }, + { + name: "three market ids: get two", + setup: func() { + store := s.getStore() + keeper.SetMarketKnown(store, 88) + keeper.SetMarketKnown(store, 3) + keeper.SetMarketKnown(store, 50) + }, + cb: stopAfter(2), + expMarketIDs: []uint32{3, 50}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetBuyerSettlementFlatFees() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetBuyerSettlementRatios() + marketIDs = nil + testFunc := func() { + s.k.IterateKnownMarketIDs(s.ctx, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateKnownMarketIDs") + assertEqualSlice(s, tc.expMarketIDs, marketIDs, func(marketID uint32) string { + return fmt.Sprintf("%d", marketID) + }, "IterateKnownMarketIDs market ids") + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_CalculateSellerSettlementRatioFee() +func (s *TestSuite) TestKeeper_GetCreateAskFlatFees() { + setter := keeper.SetCreateAskFlatFees + tests := []struct { + name string + setup func() + marketID uint32 + expected []sdk.Coin + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 2, []sdk.Coin{s.coin("5avocado")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("5avocado")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("1acorn")}) + setter(store, 2, []sdk.Coin{s.coin("8plum"), s.coin("2apple")}) + setter(store, 3, []sdk.Coin{s.coin("3acorn")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("2apple"), s.coin("8plum")}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CalculateBuyerSettlementRatioFeeOptions() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateCreateAskFlatFee() + var actual []sdk.Coin + testFunc := func() { + actual = s.k.GetCreateAskFlatFees(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetCreateAskFlatFees(%d)", tc.marketID) + s.Assert().Equal(s.coinsString(tc.expected), s.coinsString(actual), + "GetCreateAskFlatFees(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateCreateBidFlatFee() +func (s *TestSuite) TestKeeper_GetCreateBidFlatFees() { + setter := keeper.SetCreateBidFlatFees + tests := []struct { + name string + setup func() + marketID uint32 + expected []sdk.Coin + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 2, []sdk.Coin{s.coin("5avocado")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("5avocado")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("1acorn")}) + setter(store, 2, []sdk.Coin{s.coin("8plum"), s.coin("2apple")}) + setter(store, 3, []sdk.Coin{s.coin("3acorn")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("2apple"), s.coin("8plum")}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateSellerSettlementFlatFee() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateAskPrice() + var actual []sdk.Coin + testFunc := func() { + actual = s.k.GetCreateBidFlatFees(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetCreateBidFlatFees(%d)", tc.marketID) + s.Assert().Equal(s.coinsString(tc.expected), s.coinsString(actual), + "GetCreateBidFlatFees(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateBuyerSettlementFee() +func (s *TestSuite) TestKeeper_GetSellerSettlementFlatFees() { + setter := keeper.SetSellerSettlementFlatFees + tests := []struct { + name string + setup func() + marketID uint32 + expected []sdk.Coin + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 2, []sdk.Coin{s.coin("5avocado")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("5avocado")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("1acorn")}) + setter(store, 2, []sdk.Coin{s.coin("8plum"), s.coin("2apple")}) + setter(store, 3, []sdk.Coin{s.coin("3acorn")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("2apple"), s.coin("8plum")}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdateFees() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_IsMarketActive() + var actual []sdk.Coin + testFunc := func() { + actual = s.k.GetSellerSettlementFlatFees(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetSellerSettlementFlatFees(%d)", tc.marketID) + s.Assert().Equal(s.coinsString(tc.expected), s.coinsString(actual), + "GetSellerSettlementFlatFees(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdateMarketActive() +func (s *TestSuite) TestKeeper_GetSellerSettlementRatios() { + setter := keeper.SetSellerSettlementRatios + tests := []struct { + name string + setup func() + marketID uint32 + expected []exchange.FeeRatio + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 2, []exchange.FeeRatio{s.ratio("50pear:3fig")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: []exchange.FeeRatio{s.ratio("50pear:3fig")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 2, []exchange.FeeRatio{ + s.ratio("50pear:3fig"), + s.ratio("100apple:7grape"), + }) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: []exchange.FeeRatio{ + s.ratio("100apple:7grape"), + s.ratio("50pear:3fig"), + }, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_IsUserSettlementAllowed() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdateUserSettlementAllowed() + var actual []exchange.FeeRatio + testFunc := func() { + actual = s.k.GetSellerSettlementRatios(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetSellerSettlementRatios(%d)", tc.marketID) + s.Assert().Equal(s.ratiosStrings(tc.expected), s.ratiosStrings(actual), + "GetSellerSettlementRatios(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_HasPermission() +func (s *TestSuite) TestKeeper_GetBuyerSettlementFlatFees() { + setter := keeper.SetBuyerSettlementFlatFees + tests := []struct { + name string + setup func() + marketID uint32 + expected []sdk.Coin + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("8acorn")}) + setter(store, 2, []sdk.Coin{s.coin("5avocado")}) + setter(store, 3, []sdk.Coin{s.coin("3apple")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("5avocado")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []sdk.Coin{s.coin("1acorn")}) + setter(store, 2, []sdk.Coin{s.coin("8plum"), s.coin("2apple")}) + setter(store, 3, []sdk.Coin{s.coin("3acorn")}) + }, + marketID: 2, + expected: []sdk.Coin{s.coin("2apple"), s.coin("8plum")}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanSettleOrders() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanSetIDs() + var actual []sdk.Coin + testFunc := func() { + actual = s.k.GetBuyerSettlementFlatFees(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetBuyerSettlementFlatFees(%d)", tc.marketID) + s.Assert().Equal(s.coinsString(tc.expected), s.coinsString(actual), + "GetBuyerSettlementFlatFees(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_CanCancelOrdersForMarket() +func (s *TestSuite) TestKeeper_GetBuyerSettlementRatios() { + setter := keeper.SetBuyerSettlementRatios + tests := []struct { + name string + setup func() + marketID uint32 + expected []exchange.FeeRatio + }{ + { + name: "no entries at all", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "no entries for market", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one entry", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 2, []exchange.FeeRatio{s.ratio("50pear:3fig")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: []exchange.FeeRatio{s.ratio("50pear:3fig")}, + }, + { + name: "market with two coins", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1fig")}) + setter(store, 2, []exchange.FeeRatio{ + s.ratio("50pear:3fig"), + s.ratio("100apple:7grape"), + }) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1fig")}) + }, + marketID: 2, + expected: []exchange.FeeRatio{ + s.ratio("100apple:7grape"), + s.ratio("50pear:3fig"), + }, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanWithdrawMarketFunds() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanUpdateMarket() + var actual []exchange.FeeRatio + testFunc := func() { + actual = s.k.GetBuyerSettlementRatios(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetBuyerSettlementRatios(%d)", tc.marketID) + s.Assert().Equal(s.ratiosStrings(tc.expected), s.ratiosStrings(actual), + "GetBuyerSettlementRatios(%d)", tc.marketID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_CanManagePermissions() +func (s *TestSuite) TestKeeper_CalculateSellerSettlementRatioFee() { + setter := keeper.SetSellerSettlementRatios + tests := []struct { + name string + setup func() + marketID uint32 + price sdk.Coin + expFee *sdk.Coin + expErr string + }{ + { + name: "no ratios in store", + setup: nil, + marketID: 1, + price: s.coin("100plum"), + expFee: nil, + expErr: "", + }, + { + name: "no ratios for market", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1peach")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + }, + marketID: 2, + price: s.coin("100plum"), + expFee: nil, + expErr: "", + }, + { + name: "no ratio for price denom", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1peach")}) + setter(store, 2, []exchange.FeeRatio{ + s.ratio("10prune:1prune"), + s.ratio("50pear:3pear"), + }) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + }, + marketID: 2, + price: s.coin("100pears"), + expErr: "no seller settlement fee ratio found for denom \"pears\"", + }, + { + name: "ratio evenly applicable", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1peach")}) + setter(store, 2, []exchange.FeeRatio{s.ratio("50pear:3pear")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + }, + marketID: 2, + price: s.coin("350pear"), + expFee: s.coinP("21pear"), + }, + { + name: "ratio not evenly applicable", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("8peach:1peach")}) + setter(store, 2, []exchange.FeeRatio{s.ratio("50pear:3pear")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + }, + marketID: 2, + price: s.coin("442pear"), + expFee: s.coinP("27pear"), + }, + { + name: "error applying ratio", + setup: func() { + setter(s.getStore(), 1, []exchange.FeeRatio{s.ratio("0peach:1peach")}) + }, + marketID: 1, + price: s.coin("100peach"), + expErr: "invalid seller settlement fees: cannot apply ratio 0peach:1peach to price 100peach: division by zero", + }, + { + name: "three ratios: first", + setup: func() { + setter(s.getStore(), 8, []exchange.FeeRatio{ + s.ratio("10plum:1plum"), + s.ratio("25prune:2prune"), + s.ratio("50pear:3pear"), + }) + }, + marketID: 8, + price: s.coin("500plum"), + expFee: s.coinP("50plum"), // 500 * 1 = 500, 500 / 10 = 50 => 50. + }, + { + name: "three ratios: second", + setup: func() { + setter(s.getStore(), 777, []exchange.FeeRatio{ + s.ratio("10plum:1plum"), + s.ratio("25prune:2prune"), + s.ratio("50pear:3pear"), + }) + }, + marketID: 777, + price: s.coin("732prune"), + expFee: s.coinP("59prune"), // 732 * 2 = 1464, 1464 / 25 = 58.56 => 59. + }, + { + name: "three ratios: third", + setup: func() { + setter(s.getStore(), 41, []exchange.FeeRatio{ + s.ratio("10plum:1plum"), + s.ratio("25prune:2prune"), + s.ratio("50pear:3pear"), + }) + }, + marketID: 41, + price: s.coin("123456pear"), + expFee: s.coinP("7408pear"), // 123456 * 3 = 370368, 370368 / 50 = 7407.36 => 7408. + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanManageReqAttrs() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetUserPermissions() + var fee *sdk.Coin + var err error + testFunc := func() { + fee, err = s.k.CalculateSellerSettlementRatioFee(s.ctx, tc.marketID, tc.price) + } + s.Require().NotPanics(testFunc, "CalculateSellerSettlementRatioFee(%d, %q)", tc.marketID, tc.price) + s.assertErrorValue(err, tc.expErr, "CalculateSellerSettlementRatioFee(%d, %q)", tc.marketID, tc.price) + s.Assert().Equal(s.coinPString(tc.expFee), s.coinPString(fee), + "CalculateSellerSettlementRatioFee(%d, %q)", tc.marketID, tc.price) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_GetAccessGrants() +func (s *TestSuite) TestKeeper_CalculateBuyerSettlementRatioFeeOptions() { + setter := keeper.SetBuyerSettlementRatios + noDivErr := func(ratio, price string) string { + return fmt.Sprintf("buyer settlement fees: cannot apply ratio %s to price %s: price amount cannot be evenly divided by ratio price", + ratio, price) + } + noRatiosErr := func(price string) string { + return "no applicable buyer settlement fee ratios found for price " + price + } -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdatePermissions() + tests := []struct { + name string + setup func() + marketID uint32 + price sdk.Coin + expOpts []sdk.Coin + expErr string + }{ + { + name: "no ratios in state", + setup: nil, + marketID: 6, + price: s.coin("100peach"), + expOpts: nil, + expErr: "", + }, + { + name: "no ratios for market", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("11plum:1fig")}) + setter(store, 3, []exchange.FeeRatio{s.ratio("33prune:2grape")}) + }, + marketID: 2, + price: s.coin("100peach"), + expOpts: nil, + expErr: "", + }, + { + name: "no ratios for price denom: fee denom", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("11plum:1fig")}) + setter(store, 2, []exchange.FeeRatio{ + s.ratio("21pineapple:1fig"), + s.ratio("22pear:3fig"), + s.ratio("23peach:4fig"), + }) + setter(store, 3, []exchange.FeeRatio{s.ratio("33prune:2grape")}) + }, + marketID: 2, + price: s.coin("100fig"), + expErr: "no buyer settlement fee ratios found for denom \"fig\"", + }, + { + name: "no ratios for price denom: other market's denom", + setup: func() { + store := s.getStore() + setter(store, 1, []exchange.FeeRatio{s.ratio("11plum:1fig")}) + setter(store, 2, []exchange.FeeRatio{ + s.ratio("21pineapple:1fig"), + s.ratio("22pear:3fig"), + s.ratio("23peach:4fig"), + }) + setter(store, 3, []exchange.FeeRatio{s.ratio("33prune:2grape")}) + }, + marketID: 2, + price: s.coin("100prune"), + expErr: "no buyer settlement fee ratios found for denom \"prune\"", + }, + { + name: "one ratio: evenly divisible", + setup: func() { + setter(s.getStore(), 15, []exchange.FeeRatio{s.ratio("500pineapple:1fig")}) + }, + marketID: 15, + price: s.coin("7500pineapple"), + expOpts: []sdk.Coin{s.coin("15fig")}, + }, + { + name: "one ratio: not evenly divisible", + setup: func() { + setter(s.getStore(), 15, []exchange.FeeRatio{s.ratio("500pineapple:1fig")}) + }, + marketID: 15, + price: s.coin("7503pineapple"), + expErr: s.joinErrs( + noDivErr("500pineapple:1fig", "7503pineapple"), + noRatiosErr("7503pineapple"), + ), + }, + { + name: "three ratios for denom: none divisible", + setup: func() { + setter(s.getStore(), 21, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 21, + price: s.coin("3000plum"), + expErr: s.joinErrs( + noDivErr("123plum:1fig", "3000plum"), + noDivErr("234plum:5grape", "3000plum"), + noDivErr("345plum:7honeydew", "3000plum"), + noRatiosErr("3000plum"), + ), + }, + { + name: "three ratios for denom: only first divisible", + setup: func() { + setter(s.getStore(), 21, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 21, + price: s.coin("615plum"), + expOpts: []sdk.Coin{s.coin("5fig")}, + }, + { + name: "three ratios for denom: only second divisible", + setup: func() { + setter(s.getStore(), 99, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 99, + price: s.coin("1170plum"), + expOpts: []sdk.Coin{s.coin("25grape")}, + }, + { + name: "three ratios for denom: only third divisible", + setup: func() { + setter(s.getStore(), 3, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 3, + price: s.coin("1725plum"), + expOpts: []sdk.Coin{s.coin("35honeydew")}, + }, + { + name: "three ratios for denom: first not divisible", + setup: func() { + setter(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 1, + price: s.coin("26910plum"), + expOpts: []sdk.Coin{s.coin("575grape"), s.coin("546honeydew")}, + }, + { + name: "three ratios for denom: second not divisible", + setup: func() { + setter(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 1, + price: s.coin("50100peach"), + expOpts: []sdk.Coin{s.coin("1503fig"), s.coin("2171honeydew")}, + }, + { + name: "three ratios for denom: third not divisible", + setup: func() { + setter(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 1, + price: s.coin("50200peach"), + expOpts: []sdk.Coin{s.coin("1506fig"), s.coin("2761grape")}, + }, + { + name: "three ratios for denom: all divisible", + setup: func() { + setter(s.getStore(), 5, []exchange.FeeRatio{ + s.ratio("123plum:1fig"), + s.ratio("234plum:5grape"), + s.ratio("345plum:7honeydew"), + s.ratio("100peach:3fig"), + s.ratio("200peach:11grape"), + s.ratio("300peach:13honeydew"), + }) + }, + marketID: 5, + price: s.coin("6000peach"), + expOpts: []sdk.Coin{s.coin("180fig"), s.coin("330grape"), s.coin("260honeydew")}, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetReqAttrsAsk() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetReqAttrsBid() + var opts []sdk.Coin + var err error + testFunc := func() { + opts, err = s.k.CalculateBuyerSettlementRatioFeeOptions(s.ctx, tc.marketID, tc.price) + } + s.Require().NotPanics(testFunc, "CalculateBuyerSettlementRatioFeeOptions(%d, %q)", tc.marketID, tc.price) + s.assertErrorValue(err, tc.expErr, "CalculateBuyerSettlementRatioFeeOptions(%d, %q)", tc.marketID, tc.price) + s.Assert().Equal(s.coinsString(tc.expOpts), s.coinsString(opts), + "CalculateBuyerSettlementRatioFeeOptions(%d, %q)", tc.marketID, tc.price) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_CanCreateAsk() +func (s *TestSuite) TestKeeper_ValidateCreateAskFlatFee() { + setter := keeper.SetCreateAskFlatFees + name := "ask order creation" + nilFeeErr := func(opts string) string { + return fmt.Sprintf("no %s fee provided, must be one of: %s", name, opts) + } + noFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("invalid %s fee %q, must be one of: %s", name, fee, opts) + } + lowFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("insufficient %s fee: %q is less than required amount %q", name, fee, opts) + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CanCreateBid() + tests := []struct { + name string + setup func() + marketID uint32 + fee *sdk.Coin + expErr string + }{ + { + name: "no fees in store: nil", + setup: nil, + marketID: 1, + fee: nil, + expErr: "", + }, + { + name: "no fees in store: not nil", + setup: nil, + marketID: 1, + fee: s.coinP("8fig"), + expErr: "", + }, + { + name: "no fees for market: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: nil, + expErr: "", + }, + { + name: "no fees for market: not nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: s.coinP("30fig"), + expErr: "", + }, + { + name: "one fee option: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: nil, + expErr: nilFeeErr("11fig"), + }, + { + name: "one fee option: diff denom", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("5grape"), + expErr: noFeeErr("5grape", "11fig"), + }, + { + name: "one fee option: insufficient", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("10fig"), + expErr: lowFeeErr("10fig", "11fig"), + }, + { + name: "one fee option: same", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("11fig"), + expErr: "", + }, + { + name: "one fee option: more", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("12fig"), + expErr: "", + }, + { + name: "three fee options: nil", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: nil, + expErr: nilFeeErr("10fig,3grape,7honeydew"), + }, + { + name: "three fee options: wrong denom", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("80apple"), + expErr: noFeeErr("80apple", "10fig,3grape,7honeydew"), + }, + { + name: "three fee options: first, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("9fig"), + expErr: lowFeeErr("9fig", "10fig"), + }, + { + name: "three fee options: first, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("10fig"), + expErr: "", + }, + { + name: "three fee options: second, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("2grape"), + expErr: lowFeeErr("2grape", "3grape"), + }, + { + name: "three fee options: second, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("3grape"), + expErr: "", + }, + { + name: "three fee options: third, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("6honeydew"), + expErr: lowFeeErr("6honeydew", "7honeydew"), + }, + { + name: "three fee options: third, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("7honeydew"), + expErr: "", + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdateReqAttrs() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetMarketAccount() + var err error + testFunc := func() { + err = s.k.ValidateCreateAskFlatFee(s.ctx, tc.marketID, tc.fee) + } + s.Require().NotPanics(testFunc, "ValidateCreateAskFlatFee(%d, %s)", tc.marketID, s.coinPString(tc.fee)) + s.assertErrorValue(err, tc.expErr, "ValidateCreateAskFlatFee(%d, %s) error", tc.marketID, s.coinPString(tc.fee)) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_GetMarketDetails() +func (s *TestSuite) TestKeeper_ValidateCreateBidFlatFee() { + setter := keeper.SetCreateBidFlatFees + name := "bid order creation" + nilFeeErr := func(opts string) string { + return fmt.Sprintf("no %s fee provided, must be one of: %s", name, opts) + } + noFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("invalid %s fee %q, must be one of: %s", name, fee, opts) + } + lowFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("insufficient %s fee: %q is less than required amount %q", name, fee, opts) + } -// TODO[1658]: func (s *TestSuite) TestKeeper_UpdateMarketDetails() + tests := []struct { + name string + setup func() + marketID uint32 + fee *sdk.Coin + expErr string + }{ + { + name: "no fees in store: nil", + setup: nil, + marketID: 1, + fee: nil, + expErr: "", + }, + { + name: "no fees in store: not nil", + setup: nil, + marketID: 1, + fee: s.coinP("8fig"), + expErr: "", + }, + { + name: "no fees for market: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: nil, + expErr: "", + }, + { + name: "no fees for market: not nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: s.coinP("30fig"), + expErr: "", + }, + { + name: "one fee option: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: nil, + expErr: nilFeeErr("11fig"), + }, + { + name: "one fee option: diff denom", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("5grape"), + expErr: noFeeErr("5grape", "11fig"), + }, + { + name: "one fee option: insufficient", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("10fig"), + expErr: lowFeeErr("10fig", "11fig"), + }, + { + name: "one fee option: same", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("11fig"), + expErr: "", + }, + { + name: "one fee option: more", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("12fig"), + expErr: "", + }, + { + name: "three fee options: nil", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: nil, + expErr: nilFeeErr("10fig,3grape,7honeydew"), + }, + { + name: "three fee options: wrong denom", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("80apple"), + expErr: noFeeErr("80apple", "10fig,3grape,7honeydew"), + }, + { + name: "three fee options: first, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("9fig"), + expErr: lowFeeErr("9fig", "10fig"), + }, + { + name: "three fee options: first, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("10fig"), + expErr: "", + }, + { + name: "three fee options: second, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("2grape"), + expErr: lowFeeErr("2grape", "3grape"), + }, + { + name: "three fee options: second, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("3grape"), + expErr: "", + }, + { + name: "three fee options: third, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("6honeydew"), + expErr: lowFeeErr("6honeydew", "7honeydew"), + }, + { + name: "three fee options: third, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("7honeydew"), + expErr: "", + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CreateMarket() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetMarket() + var err error + testFunc := func() { + err = s.k.ValidateCreateBidFlatFee(s.ctx, tc.marketID, tc.fee) + } + s.Require().NotPanics(testFunc, "ValidateCreateBidFlatFee(%d, %s)", tc.marketID, s.coinPString(tc.fee)) + s.assertErrorValue(err, tc.expErr, "ValidateCreateBidFlatFee(%d, %s) error", tc.marketID, s.coinPString(tc.fee)) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateMarkets() +func (s *TestSuite) TestKeeper_ValidateSellerSettlementFlatFee() { + setter := keeper.SetSellerSettlementFlatFees + name := "seller settlement flat" + nilFeeErr := func(opts string) string { + return fmt.Sprintf("no %s fee provided, must be one of: %s", name, opts) + } + noFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("invalid %s fee %q, must be one of: %s", name, fee, opts) + } + lowFeeErr := func(fee string, opts string) string { + return fmt.Sprintf("insufficient %s fee: %q is less than required amount %q", name, fee, opts) + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetMarketBrief() + tests := []struct { + name string + setup func() + marketID uint32 + fee *sdk.Coin + expErr string + }{ + { + name: "no fees in store: nil", + setup: nil, + marketID: 1, + fee: nil, + expErr: "", + }, + { + name: "no fees in store: not nil", + setup: nil, + marketID: 1, + fee: s.coinP("8fig"), + expErr: "", + }, + { + name: "no fees for market: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: nil, + expErr: "", + }, + { + name: "no fees for market: not nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("2grape")}) + }, + marketID: 6, + fee: s.coinP("30fig"), + expErr: "", + }, + { + name: "one fee option: nil", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: nil, + expErr: nilFeeErr("11fig"), + }, + { + name: "one fee option: diff denom", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("5grape"), + expErr: noFeeErr("5grape", "11fig"), + }, + { + name: "one fee option: insufficient", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("10fig"), + expErr: lowFeeErr("10fig", "11fig"), + }, + { + name: "one fee option: same", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("11fig"), + expErr: "", + }, + { + name: "one fee option: more", + setup: func() { + store := s.getStore() + setter(store, 5, []sdk.Coin{s.coin("10fig"), s.coin("3grape")}) + setter(store, 6, []sdk.Coin{s.coin("11fig")}) + setter(store, 7, []sdk.Coin{s.coin("12fig"), s.coin("1grape")}) + }, + marketID: 6, + fee: s.coinP("12fig"), + expErr: "", + }, + { + name: "three fee options: nil", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: nil, + expErr: nilFeeErr("10fig,3grape,7honeydew"), + }, + { + name: "three fee options: wrong denom", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("80apple"), + expErr: noFeeErr("80apple", "10fig,3grape,7honeydew"), + }, + { + name: "three fee options: first, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("9fig"), + expErr: lowFeeErr("9fig", "10fig"), + }, + { + name: "three fee options: first, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("10fig"), + expErr: "", + }, + { + name: "three fee options: second, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("2grape"), + expErr: lowFeeErr("2grape", "3grape"), + }, + { + name: "three fee options: second, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("3grape"), + expErr: "", + }, + { + name: "three fee options: third, low", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("6honeydew"), + expErr: lowFeeErr("6honeydew", "7honeydew"), + }, + { + name: "three fee options: third, ok", + setup: func() { + setter(s.getStore(), 8, []sdk.Coin{s.coin("10fig"), s.coin("3grape"), s.coin("7honeydew")}) + }, + marketID: 8, + fee: s.coinP("7honeydew"), + expErr: "", + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_WithdrawMarketFunds() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_ValidateMarket() + var err error + testFunc := func() { + err = s.k.ValidateSellerSettlementFlatFee(s.ctx, tc.marketID, tc.fee) + } + s.Require().NotPanics(testFunc, "ValidateSellerSettlementFlatFee(%d, %s)", tc.marketID, s.coinPString(tc.fee)) + s.assertErrorValue(err, tc.expErr, "ValidateSellerSettlementFlatFee(%d, %s) error", tc.marketID, s.coinPString(tc.fee)) + }) + } +} + +func (s *TestSuite) TestKeeper_ValidateAskPrice() { + tests := []struct { + name string + setup func() + marketID uint32 + price sdk.Coin + settlementFlatFee *sdk.Coin + expErr string + }{ + { + name: "no ratios in store", + setup: nil, + marketID: 1, + price: s.coin("1plum"), + settlementFlatFee: nil, + expErr: "", + }, + { + name: "no ratios in market: no flat", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("11plum:1plum")}) + }, + marketID: 2, + price: s.coin("1plum"), + settlementFlatFee: nil, + expErr: "", + }, + { + name: "no ratios in market: price less than flat", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("11plum:1plum")}) + }, + marketID: 2, + price: s.coin("1plum"), + settlementFlatFee: s.coinP("2plum"), + expErr: "price 1plum is not more than seller settlement flat fee 2plum", + }, + { + name: "no ratios in market: price equals flat", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("11plum:1plum")}) + }, + marketID: 2, + price: s.coin("2plum"), + settlementFlatFee: s.coinP("2plum"), + expErr: "price 2plum is not more than seller settlement flat fee 2plum", + }, + { + name: "no ratios in market: price more than flat", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("11plum:1plum")}) + }, + marketID: 2, + price: s.coin("3plum"), + settlementFlatFee: s.coinP("2plum"), + expErr: "", + }, + { + name: "no ratios in market: fee diff denom with larger amount", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("11plum:1plum")}) + }, + marketID: 2, + price: s.coin("2plum"), + settlementFlatFee: s.coinP("3fig"), + expErr: "", + }, + { + name: "one ratio: wrong denom", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("500peach"), + settlementFlatFee: nil, + expErr: "no seller settlement fee ratio found for denom \"peach\"", + }, + { + name: "one ratio: no flat: price less than ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:13plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: nil, + expErr: "price 12plum is not more than seller settlement ratio fee 13plum", + }, + { + name: "one ratio: no flat: price equals ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:11plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("11plum"), + settlementFlatFee: nil, + expErr: "price 11plum is not more than seller settlement ratio fee 11plum", + }, + { + name: "one ratio: no flat: price more than ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:11plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("13plum"), + settlementFlatFee: nil, + expErr: "", + }, + { + name: "one ratio: diff flat: price less than ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:13plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: s.coinP("20peach"), + expErr: "price 12plum is not more than seller settlement ratio fee 13plum", + }, + { + name: "one ratio: diff flat: price equals ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:11plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("11plum"), + settlementFlatFee: s.coinP("20peach"), + expErr: "price 11plum is not more than seller settlement ratio fee 11plum", + }, + { + name: "one ratio: diff flat: price more than ratio", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:11plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: s.coinP("20peach"), + expErr: "", + }, + { + name: "one ratio: price more than flat, more than ratio, less than total", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:11plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: s.coinP("2plum"), + expErr: "price 12plum is not more than total required seller settlement fee 13plum = 2plum flat + 11plum ratio", + }, + { + name: "one ratio: price equals total", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:7plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: s.coinP("5plum"), + expErr: "price 12plum is not more than total required seller settlement fee 12plum = 5plum flat + 7plum ratio", + }, + { + name: "one ratio: price more than total", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:7plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("12plum"), + settlementFlatFee: s.coinP("4plum"), + expErr: "", + }, + { + name: "ratio cannot be evenly applied to price, but is enough", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("10plum:1plum")}) + keeper.SetSellerSettlementRatios(store, 2, []exchange.FeeRatio{s.ratio("12plum:7plum")}) + keeper.SetSellerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("15plum:1plum")}) + }, + marketID: 2, + price: s.coin("123plum"), + settlementFlatFee: nil, + expErr: "", + }, + { + name: "error applying ratio", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, []exchange.FeeRatio{s.ratio("0plum:1plum")}) + }, + marketID: 1, + price: s.coin("100plum"), + settlementFlatFee: nil, + expErr: "cannot apply ratio 0plum:1plum to price 100plum: division by zero", + }, + { + name: "three ratios: wrong denom", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("100peach:1peach"), + s.ratio("200pear:3pear"), + s.ratio("300plum:7plum"), + }) + }, + marketID: 1, + price: s.coin("5000prune"), + settlementFlatFee: nil, + expErr: "no seller settlement fee ratio found for denom \"prune\"", + }, + { + name: "three ratios: price less than total", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("5000peach:1peach"), + s.ratio("200pear:199pear"), + s.ratio("5000plum:7plum"), + }) + }, + marketID: 1, + price: s.coin("20pear"), + settlementFlatFee: s.coinP("1pear"), + expErr: "price 20pear is not more than total required seller settlement fee 21pear = 1pear flat + 20pear ratio", + }, + { + name: "three ratios: price more", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, []exchange.FeeRatio{ + s.ratio("100peach:1peach"), + s.ratio("200pear:3pear"), + s.ratio("300plum:7plum"), + }) + }, + marketID: 1, + price: s.coin("5000pear"), + settlementFlatFee: nil, + expErr: "", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var err error + testFunc := func() { + err = s.k.ValidateAskPrice(s.ctx, tc.marketID, tc.price, tc.settlementFlatFee) + } + s.Require().NotPanics(testFunc, "ValidateAskPrice(%d, %q, %s)", + tc.marketID, tc.price, s.coinPString(tc.settlementFlatFee)) + s.assertErrorValue(err, tc.expErr, "ValidateAskPrice(%d, %q, %s)", + tc.marketID, tc.price, s.coinPString(tc.settlementFlatFee)) + }) + } +} + +func (s *TestSuite) TestKeeper_ValidateBuyerSettlementFee() { + noFeeErr := "insufficient buyer settlement fee: no fee provided" + flatErr := func(opts string) string { + return "required flat fee not satisfied, valid options: " + opts + } + ratioErr := func(opts string) string { + return "required ratio fee not satisfied, valid ratios: " + opts + } + insufficientErr := func(fee string) string { + return "insufficient buyer settlement fee " + fee + } + + tests := []struct { + name string + setup func() + marketID uint32 + price sdk.Coin + fee sdk.Coins + expErr string + }{ + { + name: "empty state: no fee", + setup: nil, + marketID: 8, + price: s.coin("50peach"), + fee: nil, + expErr: "", + }, + { + name: "empty state: with fee", + setup: nil, + marketID: 8, + price: s.coin("100peach"), + fee: s.coins("120peach"), // This is okay because it's added to the price. + expErr: "", + }, + { + name: "no flat no ratio: no fee", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 1, s.coins("10peach,12plum")) + keeper.SetBuyerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("100peach:3fig")}) + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("14peach,8plum")) + keeper.SetBuyerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("100peach:1grape")}) + }, + marketID: 2, + price: s.coin("5000peach"), + fee: nil, + expErr: "", + }, + { + name: "no flat no ratio: with fee", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 1, s.coins("10peach,12plum")) + keeper.SetBuyerSettlementRatios(store, 1, []exchange.FeeRatio{s.ratio("100peach:3fig")}) + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("14peach,8plum")) + keeper.SetBuyerSettlementRatios(store, 3, []exchange.FeeRatio{s.ratio("100peach:1grape")}) + }, + marketID: 2, + price: s.coin("5000peach"), + fee: s.coins("5001peach"), // This is okay because it's added to the price. + expErr: "", + }, + { + name: "only flat: no fee", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("11peach,9plum")) + }, + marketID: 2, + price: s.coin("54pear"), + fee: nil, + expErr: s.joinErrs( + flatErr("11peach,9plum"), + noFeeErr, + ), + }, + { + name: "only flat: wrong denom", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("11peach,9plum")) + }, + marketID: 2, + price: s.coin("54pear"), + fee: s.coins("3pear"), + expErr: s.joinErrs( + "no flat fee options available for denom pear", + flatErr("11peach,9plum"), + insufficientErr("3pear"), + ), + }, + { + name: "only flat: less than req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("11peach,9plum")) + }, + marketID: 2, + price: s.coin("54pear"), + fee: s.coins("10peach"), + expErr: s.joinErrs( + "10peach is less than required flat fee 11peach", + flatErr("11peach,9plum"), + insufficientErr("10peach"), + ), + }, + { + name: "only flat: equals req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("11peach,9plum")) + }, + marketID: 2, + price: s.coin("54pear"), + fee: s.coins("11peach"), + expErr: "", + }, + { + name: "only flat: more than req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("11peach,9plum")) + }, + marketID: 2, + price: s.coin("54pear"), + fee: s.coins("10peach,10plum"), + expErr: "", + }, + { + name: "only ratio: nofee", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("54pear"), + fee: nil, + expErr: s.joinErrs( + ratioErr("100peach:3fig,100peach:1grape"), + noFeeErr, + ), + }, + { + name: "only ratio: wrong price denom", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500pear"), + fee: s.coins("5grape"), + expErr: s.joinErrs( + "no ratio from price denom pear to fee denom grape", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("5grape"), + ), + }, + { + name: "only ratio: wrong fee denom", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("20honeydew"), + expErr: s.joinErrs( + "no ratio from price denom peach to fee denom honeydew", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("20honeydew"), + ), + }, + { + name: "only ratio: less than req", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("14fig,4grape"), + expErr: s.joinErrs( + "14fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + "4grape is less than required ratio fee 5grape (based on price 500peach and ratio 100peach:1grape)", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("14fig,4grape"), + ), + }, + { + name: "only ratio: equals req", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("5grape"), + expErr: "", + }, + { + name: "only ratio: more than req", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("16fig"), + expErr: "", + }, + { + name: "both: no fee", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: nil, + expErr: s.joinErrs( + flatErr("10fig,2honeydew"), + ratioErr("100peach:3fig,100peach:1grape"), + noFeeErr, + ), + }, + { + name: "both: no flat denom", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("5grape"), + expErr: s.joinErrs( + "no flat fee options available for denom grape", + flatErr("10fig,2honeydew"), + insufficientErr("5grape"), + ), + }, + { + name: "both: no ratio denom", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("2honeydew"), + expErr: s.joinErrs( + "no ratio from price denom peach to fee denom honeydew", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("2honeydew"), + ), + }, + { + name: "both: neither flat nor ratio denom", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("33apple,44banana"), + expErr: s.joinErrs( + "no flat fee options available for denom apple", + "no flat fee options available for denom banana", + flatErr("10fig,2honeydew"), + "no ratio from price denom peach to fee denom apple", + "no ratio from price denom peach to fee denom banana", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("33apple,44banana"), + ), + }, + { + name: "both: one denom: less than either", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("9fig"), + expErr: s.joinErrs( + "9fig is less than required flat fee 10fig", + flatErr("10fig,2honeydew"), + "9fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("9fig"), + ), + }, + { + name: "both: one denom: less than ratio, more than flat", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("14fig"), + expErr: s.joinErrs( + "14fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("14fig"), + ), + }, + { + name: "both: one denom: less than flat, more than ratio", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("300peach"), + fee: s.coins("9fig"), + expErr: s.joinErrs( + "9fig is less than required flat fee 10fig", + flatErr("10fig,2honeydew"), + insufficientErr("9fig"), + ), + }, + { + name: "both: one denom: more than either, less than total req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("24fig"), + expErr: s.joinErrs( + "24fig is less than combined fee 25fig = 10fig (flat) + 15fig (ratio based on price 500peach)", + insufficientErr("24fig"), + ), + }, + { + name: "both: one denom: fee equals total req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("25fig"), + expErr: "", + }, + { + name: "both: one denom: fee more than total req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,2honeydew")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("26fig"), + expErr: "", + }, + { + name: "both: diff denoms: all less than req", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("9fig,4grape,80honeydew"), + expErr: s.joinErrs( + "9fig is less than required flat fee 10fig", + "4grape is less than required flat fee 6grape", + "no flat fee options available for denom honeydew", + flatErr("10fig,6grape"), + "9fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + "4grape is less than required ratio fee 5grape (based on price 500peach and ratio 100peach:1grape)", + "no ratio from price denom peach to fee denom honeydew", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("9fig,4grape,80honeydew"), + ), + }, + { + name: "both: diff denoms: flat okay, ratio not", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,4grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("10fig,4grape,80honeydew"), + expErr: s.joinErrs( + "10fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + "4grape is less than required ratio fee 5grape (based on price 500peach and ratio 100peach:1grape)", + "no ratio from price denom peach to fee denom honeydew", + ratioErr("100peach:3fig,100peach:1grape"), + insufficientErr("10fig,4grape,80honeydew"), + ), + }, + { + name: "both: diff denoms: ratio okay, flat not", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("16fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("15fig,5grape,80honeydew"), + expErr: s.joinErrs( + "15fig is less than required flat fee 16fig", + "5grape is less than required flat fee 6grape", + "no flat fee options available for denom honeydew", + flatErr("16fig,6grape"), + insufficientErr("15fig,5grape,80honeydew"), + ), + }, + { + name: "both: diff denoms: either enough for one fee type, flat first", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("14fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("14fig,5grape"), + expErr: "", + }, + { + name: "both: diff denoms: either enough for one fee type, ratio first", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("16fig,4grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("15fig,4grape"), + expErr: "", + }, + { + name: "both: two denoms: first is more than either, less than total, second less than either", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,4grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("24fig,3grape"), + expErr: s.joinErrs( + "3grape is less than required flat fee 4grape", + "3grape is less than required ratio fee 5grape (based on price 500peach and ratio 100peach:1grape)", + "24fig is less than combined fee 25fig = 10fig (flat) + 15fig (ratio based on price 500peach)", + insufficientErr("24fig,3grape"), + ), + }, + { + name: "both: two denoms: first is more than either, less than total, second covers flat", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,4grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("24fig,4grape"), + expErr: "", + }, + { + name: "both: two denoms: first is more than either, less than total, second covers ratio", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("24fig,5grape"), + expErr: "", + }, + { + name: "both: two denoms: first less than either, second more than either, less than total", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("9fig,10grape"), + expErr: s.joinErrs( + "9fig is less than required flat fee 10fig", + "9fig is less than required ratio fee 15fig (based on price 500peach and ratio 100peach:3fig)", + "10grape is less than combined fee 11grape = 6grape (flat) + 5grape (ratio based on price 500peach)", + insufficientErr("9fig,10grape"), + ), + }, + { + name: "both: two denoms: first covers flat, second more than either, less than total", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("10fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("10fig,10grape"), + expErr: "", + }, + { + name: "both: two denoms: first covers ratio, second more than either, less than total", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 2, s.coins("16fig,6grape")) + keeper.SetBuyerSettlementRatios(s.getStore(), 2, []exchange.FeeRatio{ + s.ratio("100peach:3fig"), s.ratio("100peach:1grape"), + }) + }, + marketID: 2, + price: s.coin("500peach"), + fee: s.coins("15fig,10grape"), + expErr: "", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var err error + testFunc := func() { + err = s.k.ValidateBuyerSettlementFee(s.ctx, tc.marketID, tc.price, tc.fee) + } + s.Require().NotPanics(testFunc, "ValidateBuyerSettlementFee(%d, %q, %q)", tc.marketID, tc.price, tc.fee) + s.assertErrorValue(err, tc.expErr, "ValidateBuyerSettlementFee(%d, %q, %q)", tc.marketID, tc.price, tc.fee) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdateFees() { + type marketFees struct { + marketID uint32 + createAsk string + createBid string + sellerFlat string + sellerRatio string + buyerFlat string + buyerRatio string + } + getMarketFees := func(marketID uint32) marketFees { + return marketFees{ + marketID: marketID, + createAsk: sdk.Coins(s.k.GetCreateAskFlatFees(s.ctx, marketID)).String(), + createBid: sdk.Coins(s.k.GetCreateBidFlatFees(s.ctx, marketID)).String(), + sellerFlat: sdk.Coins(s.k.GetSellerSettlementFlatFees(s.ctx, marketID)).String(), + sellerRatio: exchange.FeeRatiosString(s.k.GetSellerSettlementRatios(s.ctx, marketID)), + buyerFlat: sdk.Coins(s.k.GetBuyerSettlementFlatFees(s.ctx, marketID)).String(), + buyerRatio: exchange.FeeRatiosString(s.k.GetBuyerSettlementRatios(s.ctx, marketID)), + } + } + + tests := []struct { + name string + setup func() + msg *exchange.MsgGovManageFeesRequest + expFees marketFees + expNoChange []uint32 + expPanic string + }{ + { + name: "nil msg", + msg: nil, + expPanic: "runtime error: invalid memory address or nil pointer dereference", + }, + + // Only create-ask flat fee changes. + { + name: "create ask: add one", + setup: func() { + keeper.SetCreateAskFlatFees(s.getStore(), 3, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeCreateAskFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, createAsk: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "create ask: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 3, s.coins("10fig")) + keeper.SetCreateAskFlatFees(store, 5, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeCreateAskFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, createAsk: "8grape"}, + expNoChange: []uint32{3}, + }, + { + name: "create ask: remove one, unknown denom", + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 3, s.coins("10grape")) + keeper.SetCreateAskFlatFees(store, 5, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeCreateAskFlat: s.coins("10grape")}, + expFees: marketFees{marketID: 5, createAsk: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "create ask: remove one, known denom, wrong amount", + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 4, s.coins("10grape")) + keeper.SetCreateAskFlatFees(store, 2, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeCreateAskFlat: s.coins("8fig")}, + expFees: marketFees{marketID: 2, createAsk: "8grape"}, + expNoChange: []uint32{4}, + }, + { + name: "create ask: add+remove with different denoms", + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 18, s.coins("10grape")) + keeper.SetCreateAskFlatFees(store, 8, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 8, + RemoveFeeCreateAskFlat: s.coins("8grape"), + AddFeeCreateAskFlat: s.coins("2honeydew"), + }, + expFees: marketFees{marketID: 8, createAsk: "10fig,2honeydew"}, + expNoChange: []uint32{18}, + }, + { + name: "create ask: add+remove with same denoms", + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 18, s.coins("10grape")) + keeper.SetCreateAskFlatFees(store, 1, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeCreateAskFlat: s.coins("10fig"), + AddFeeCreateAskFlat: s.coins("7fig"), + }, + expFees: marketFees{marketID: 1, createAsk: "7fig,8grape"}, + expNoChange: []uint32{18}, + }, + { + name: "create ask: complex", + // Remove one with wrong amount and don't replace it (10fig) + // Remove one with correct amount and replace it with another amount (7honeydew -> 5honeydew). + // Add one with a denom that already has a different amount (3cactus stomping on 7cactus) + // Add a brand new one (99plum). + // Leave one unchanged (2grape). + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 1, s.coins("10fig,4grape,2honeydew,5apple")) + keeper.SetCreateAskFlatFees(store, 2, s.coins("9fig,3grape,1honeydew,6banana")) + keeper.SetCreateAskFlatFees(store, 3, s.coins("12fig,2grape,7honeydew,7cactus")) + keeper.SetCreateAskFlatFees(store, 4, s.coins("25fig,1grape,3honeydew,8durian")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 3, + RemoveFeeCreateAskFlat: s.coins("10fig,7honeydew"), + AddFeeCreateAskFlat: s.coins("5honeydew,3cactus,99plum"), + }, + expFees: marketFees{marketID: 3, createAsk: "3cactus,2grape,5honeydew,99plum"}, + expNoChange: []uint32{1, 2, 4}, + }, + + // Only create-bid flat fee changes. + { + name: "create bid: add one", + setup: func() { + keeper.SetCreateBidFlatFees(s.getStore(), 3, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeCreateBidFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, createBid: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "create bid: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 3, s.coins("10fig")) + keeper.SetCreateBidFlatFees(store, 5, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeCreateBidFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, createBid: "8grape"}, + expNoChange: []uint32{3}, + }, + { + name: "create bid: remove one, unknown denom", + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 3, s.coins("10grape")) + keeper.SetCreateBidFlatFees(store, 5, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeCreateBidFlat: s.coins("10grape")}, + expFees: marketFees{marketID: 5, createBid: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "create bid: remove one, known denom, wrong amount", + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 4, s.coins("10grape")) + keeper.SetCreateBidFlatFees(store, 2, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeCreateBidFlat: s.coins("8fig")}, + expFees: marketFees{marketID: 2, createBid: "8grape"}, + expNoChange: []uint32{4}, + }, + { + name: "create bid: add+remove with different denoms", + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 18, s.coins("10grape")) + keeper.SetCreateBidFlatFees(store, 8, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 8, + RemoveFeeCreateBidFlat: s.coins("8grape"), + AddFeeCreateBidFlat: s.coins("2honeydew"), + }, + expFees: marketFees{marketID: 8, createBid: "10fig,2honeydew"}, + expNoChange: []uint32{18}, + }, + { + name: "create bid: add+remove with same denoms", + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 18, s.coins("10grape")) + keeper.SetCreateBidFlatFees(store, 1, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeCreateBidFlat: s.coins("10fig"), + AddFeeCreateBidFlat: s.coins("7fig"), + }, + expFees: marketFees{marketID: 1, createBid: "7fig,8grape"}, + expNoChange: []uint32{18}, + }, + { + name: "create bid: complex", + // Remove one with wrong amount and don't replace it (10fig) + // Remove one with correct amount and replace it with another amount (7honeydew -> 5honeydew). + // Add one with a denom that already has a different amount (3cactus stomping on 7cactus) + // Add a brand new one (99plum). + // Leave one unchanged (2grape). + setup: func() { + store := s.getStore() + keeper.SetCreateBidFlatFees(store, 1, s.coins("10fig,4grape,2honeydew,5apple")) + keeper.SetCreateBidFlatFees(store, 2, s.coins("9fig,3grape,1honeydew,6banana")) + keeper.SetCreateBidFlatFees(store, 3, s.coins("12fig,2grape,7honeydew,7cactus")) + keeper.SetCreateBidFlatFees(store, 4, s.coins("25fig,1grape,3honeydew,8durian")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 3, + RemoveFeeCreateBidFlat: s.coins("10fig,7honeydew"), + AddFeeCreateBidFlat: s.coins("5honeydew,3cactus,99plum"), + }, + expFees: marketFees{marketID: 3, createBid: "3cactus,2grape,5honeydew,99plum"}, + expNoChange: []uint32{1, 2, 4}, + }, + + // Only seller settlement flat fee changes. + { + name: "seller flat: add one", + setup: func() { + keeper.SetSellerSettlementFlatFees(s.getStore(), 3, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeSellerSettlementFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, sellerFlat: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "seller flat: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 3, s.coins("10fig")) + keeper.SetSellerSettlementFlatFees(store, 5, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeSellerSettlementFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, sellerFlat: "8grape"}, + expNoChange: []uint32{3}, + }, + { + name: "seller flat: remove one, unknown denom", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 3, s.coins("10grape")) + keeper.SetSellerSettlementFlatFees(store, 5, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeSellerSettlementFlat: s.coins("10grape")}, + expFees: marketFees{marketID: 5, sellerFlat: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "seller flat: remove one, known denom, wrong amount", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 4, s.coins("10grape")) + keeper.SetSellerSettlementFlatFees(store, 2, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeSellerSettlementFlat: s.coins("8fig")}, + expFees: marketFees{marketID: 2, sellerFlat: "8grape"}, + expNoChange: []uint32{4}, + }, + { + name: "seller flat: add+remove with different denoms", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 18, s.coins("10grape")) + keeper.SetSellerSettlementFlatFees(store, 8, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 8, + RemoveFeeSellerSettlementFlat: s.coins("8grape"), + AddFeeSellerSettlementFlat: s.coins("2honeydew"), + }, + expFees: marketFees{marketID: 8, sellerFlat: "10fig,2honeydew"}, + expNoChange: []uint32{18}, + }, + { + name: "seller flat: add+remove with same denoms", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 18, s.coins("10grape")) + keeper.SetSellerSettlementFlatFees(store, 1, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeSellerSettlementFlat: s.coins("10fig"), + AddFeeSellerSettlementFlat: s.coins("7fig"), + }, + expFees: marketFees{marketID: 1, sellerFlat: "7fig,8grape"}, + expNoChange: []uint32{18}, + }, + { + name: "seller flat: complex", + // Remove one with wrong amount and don't replace it (10fig) + // Remove one with correct amount and replace it with another amount (7honeydew -> 5honeydew). + // Add one with a denom that already has a different amount (3cactus stomping on 7cactus) + // Add a brand new one (99plum). + // Leave one unchanged (2grape). + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementFlatFees(store, 1, s.coins("10fig,4grape,2honeydew,5apple")) + keeper.SetSellerSettlementFlatFees(store, 2, s.coins("9fig,3grape,1honeydew,6banana")) + keeper.SetSellerSettlementFlatFees(store, 3, s.coins("12fig,2grape,7honeydew,7cactus")) + keeper.SetSellerSettlementFlatFees(store, 4, s.coins("25fig,1grape,3honeydew,8durian")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 3, + RemoveFeeSellerSettlementFlat: s.coins("10fig,7honeydew"), + AddFeeSellerSettlementFlat: s.coins("5honeydew,3cactus,99plum"), + }, + expFees: marketFees{marketID: 3, sellerFlat: "3cactus,2grape,5honeydew,99plum"}, + expNoChange: []uint32{1, 2, 4}, + }, + + // Only buyer settlement flat fee changes. + { + name: "buyer flat: add one", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 3, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeBuyerSettlementFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, buyerFlat: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer flat: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("10fig")) + keeper.SetBuyerSettlementFlatFees(store, 5, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeBuyerSettlementFlat: s.coins("10fig")}, + expFees: marketFees{marketID: 5, buyerFlat: "8grape"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer flat: remove one, unknown denom", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("10grape")) + keeper.SetBuyerSettlementFlatFees(store, 5, s.coins("10fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeBuyerSettlementFlat: s.coins("10grape")}, + expFees: marketFees{marketID: 5, buyerFlat: "10fig"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer flat: remove one, known denom, wrong amount", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 4, s.coins("10grape")) + keeper.SetBuyerSettlementFlatFees(store, 2, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeBuyerSettlementFlat: s.coins("8fig")}, + expFees: marketFees{marketID: 2, buyerFlat: "8grape"}, + expNoChange: []uint32{4}, + }, + { + name: "buyer flat: add+remove with different denoms", + setup: func() { + keeper.SetBuyerSettlementFlatFees(s.getStore(), 18, s.coins("10grape")) + keeper.SetBuyerSettlementFlatFees(s.getStore(), 8, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 8, + RemoveFeeBuyerSettlementFlat: s.coins("8grape"), + AddFeeBuyerSettlementFlat: s.coins("2honeydew"), + }, + expFees: marketFees{marketID: 8, buyerFlat: "10fig,2honeydew"}, + expNoChange: []uint32{18}, + }, + { + name: "buyer flat: add+remove with same denoms", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 18, s.coins("10grape")) + keeper.SetBuyerSettlementFlatFees(store, 1, s.coins("10fig,8grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeBuyerSettlementFlat: s.coins("10fig"), + AddFeeBuyerSettlementFlat: s.coins("7fig"), + }, + expFees: marketFees{marketID: 1, buyerFlat: "7fig,8grape"}, + expNoChange: []uint32{18}, + }, + { + name: "buyer flat: complex", + // Remove one with wrong amount and don't replace it (10fig) + // Remove one with correct amount and replace it with another amount (7honeydew -> 5honeydew). + // Add one with a denom that already has a different amount (3cactus stomping on 7cactus) + // Add a brand new one (99plum). + // Leave one unchanged (2grape). + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementFlatFees(store, 1, s.coins("10fig,4grape,2honeydew,5apple")) + keeper.SetBuyerSettlementFlatFees(store, 2, s.coins("9fig,3grape,1honeydew,6banana")) + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("12fig,2grape,7honeydew,7cactus")) + keeper.SetBuyerSettlementFlatFees(store, 4, s.coins("25fig,1grape,3honeydew,8durian")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 3, + RemoveFeeBuyerSettlementFlat: s.coins("10fig,7honeydew"), + AddFeeBuyerSettlementFlat: s.coins("5honeydew,3cactus,99plum"), + }, + expFees: marketFees{marketID: 3, buyerFlat: "3cactus,2grape,5honeydew,99plum"}, + expNoChange: []uint32{1, 2, 4}, + }, + + // Only seller settlement ratio fee changes. + { + name: "seller ratio: add one", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 3, s.ratios("100peach:3fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeSellerSettlementRatios: s.ratios("50plum:1grape")}, + expFees: marketFees{marketID: 5, sellerRatio: "50plum:1grape"}, + expNoChange: []uint32{3}, + }, + { + name: "seller ratio: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetSellerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeSellerSettlementRatios: s.ratios("90peach:2fig")}, + expFees: marketFees{marketID: 5}, + expNoChange: []uint32{3}, + }, + { + name: "seller ratio: remove one, unknown denoms", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetSellerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeSellerSettlementRatios: s.ratios("90plum:2grape")}, + expFees: marketFees{marketID: 5, sellerRatio: "90peach:2fig"}, + expNoChange: []uint32{3}, + }, + { + name: "seller ratio: remove one, known price denom", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetSellerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeSellerSettlementRatios: s.ratios("90peach:2grape")}, + expFees: marketFees{marketID: 5, sellerRatio: "90peach:2fig"}, + expNoChange: []uint32{3}, + }, + { + name: "seller ratio: remove one, known fee denom", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 4, s.ratios("100peach:3fig")) + keeper.SetSellerSettlementRatios(store, 2, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeSellerSettlementRatios: s.ratios("90plum:2fig")}, + expFees: marketFees{marketID: 2, sellerRatio: "90peach:2fig"}, + expNoChange: []uint32{4}, + }, + { + name: "seller ratio: remove one, known denoms, wrong amounts", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 4, s.ratios("100peach:3fig")) + keeper.SetSellerSettlementRatios(store, 2, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeSellerSettlementRatios: s.ratios("89peach:3fig")}, + expFees: marketFees{marketID: 2}, + expNoChange: []uint32{4}, + }, + { + name: "seller ratio: add+remove with different denoms", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 7, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetSellerSettlementRatios(store, 77, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 7, + RemoveFeeSellerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeSellerSettlementRatios: s.ratios("100plum:3honeydew"), + }, + expFees: marketFees{marketID: 7, sellerRatio: "100peach:1grape,100plum:3honeydew"}, + expNoChange: []uint32{77}, + }, + { + name: "seller ratio: add+remove with different price denom", + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 7, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetSellerSettlementRatios(store, 77, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 77, + RemoveFeeSellerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeSellerSettlementRatios: s.ratios("100plum:3fig"), + }, + expFees: marketFees{marketID: 77, sellerRatio: "100peach:1grape,100plum:3fig"}, + expNoChange: []uint32{7}, + }, + { + name: "seller ratio: add+remove with different fee denom", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeSellerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeSellerSettlementRatios: s.ratios("100peach:2honeydew"), + }, + expFees: marketFees{marketID: 1, sellerRatio: "100peach:1grape,100peach:2honeydew"}, + }, + { + name: "seller ratio: add+remove with same denoms", + setup: func() { + keeper.SetSellerSettlementRatios(s.getStore(), 1, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeSellerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeSellerSettlementRatios: s.ratios("90peach:2fig"), + }, + expFees: marketFees{marketID: 1, sellerRatio: "90peach:2fig,100peach:1grape"}, + }, + { + name: "seller ratio: complex", + // Remove one with wrong amounts and don't replace it (10plum:3fig) + // Remove and replace one to change amounts (100peach:3fig -> 90peach:2fig) + // Add one with existing denoms and different amounts (110peach:2grape stomping on 100peach:1grape) + // Add one with same price denom (100peach:1honeydew) + // Add one with same fee denom (100pear:3fig) + // Add one all new (100papaya:5guava) + // Leave on untouched (100prune:2fig) + setup: func() { + store := s.getStore() + keeper.SetSellerSettlementRatios(store, 1, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetSellerSettlementRatios(store, 11, s.ratios("20plum:2fig,100peach:3fig,100peach:1grape,100prune:2fig")) + keeper.SetSellerSettlementRatios(store, 111, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 11, + RemoveFeeSellerSettlementRatios: s.ratios("10plum:3fig,100peach:3fig"), + AddFeeSellerSettlementRatios: s.ratios("90peach:2fig,110peach:2grape,100peach:1honeydew,100pear:3fig,100papaya:5guava"), + }, + expFees: marketFees{ + marketID: 11, + sellerRatio: "100papaya:5guava,90peach:2fig,110peach:2grape,100peach:1honeydew,100pear:3fig,100prune:2fig", + }, + expNoChange: nil, + expPanic: "", + }, + + // Only buyer settlement ratio fee changes. + { + name: "buyer ratio: add one", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 3, s.ratios("100peach:3fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, AddFeeBuyerSettlementRatios: s.ratios("50plum:1grape")}, + expFees: marketFees{marketID: 5, buyerRatio: "50plum:1grape"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer ratio: remove one, exists", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetBuyerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeBuyerSettlementRatios: s.ratios("90peach:2fig")}, + expFees: marketFees{marketID: 5}, + expNoChange: []uint32{3}, + }, + { + name: "buyer ratio: remove one, unknown denoms", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetBuyerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeBuyerSettlementRatios: s.ratios("90plum:2grape")}, + expFees: marketFees{marketID: 5, buyerRatio: "90peach:2fig"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer ratio: remove one, known price denom", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 3, s.ratios("100peach:3fig")) + keeper.SetBuyerSettlementRatios(store, 5, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 5, RemoveFeeBuyerSettlementRatios: s.ratios("90peach:2grape")}, + expFees: marketFees{marketID: 5, buyerRatio: "90peach:2fig"}, + expNoChange: []uint32{3}, + }, + { + name: "buyer ratio: remove one, known fee denom", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 4, s.ratios("100peach:3fig")) + keeper.SetBuyerSettlementRatios(store, 2, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeBuyerSettlementRatios: s.ratios("90plum:2fig")}, + expFees: marketFees{marketID: 2, buyerRatio: "90peach:2fig"}, + expNoChange: []uint32{4}, + }, + { + name: "buyer ratio: remove one, known denoms, wrong amounts", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 4, s.ratios("100peach:3fig")) + keeper.SetBuyerSettlementRatios(store, 2, s.ratios("90peach:2fig")) + }, + msg: &exchange.MsgGovManageFeesRequest{MarketId: 2, RemoveFeeBuyerSettlementRatios: s.ratios("89peach:3fig")}, + expFees: marketFees{marketID: 2}, + expNoChange: []uint32{4}, + }, + { + name: "buyer ratio: add+remove with different denoms", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 7, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetBuyerSettlementRatios(store, 77, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 7, + RemoveFeeBuyerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeBuyerSettlementRatios: s.ratios("100plum:3honeydew"), + }, + expFees: marketFees{marketID: 7, buyerRatio: "100peach:1grape,100plum:3honeydew"}, + expNoChange: []uint32{77}, + }, + { + name: "buyer ratio: add+remove with different price denom", + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 7, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetBuyerSettlementRatios(store, 77, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 77, + RemoveFeeBuyerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeBuyerSettlementRatios: s.ratios("100plum:3fig"), + }, + expFees: marketFees{marketID: 77, buyerRatio: "100peach:1grape,100plum:3fig"}, + expNoChange: []uint32{7}, + }, + { + name: "buyer ratio: add+remove with different fee denom", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 1, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeBuyerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeBuyerSettlementRatios: s.ratios("100peach:2honeydew"), + }, + expFees: marketFees{marketID: 1, buyerRatio: "100peach:1grape,100peach:2honeydew"}, + }, + { + name: "buyer ratio: add+remove with same denoms", + setup: func() { + keeper.SetBuyerSettlementRatios(s.getStore(), 1, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 1, + RemoveFeeBuyerSettlementRatios: s.ratios("100peach:3fig"), + AddFeeBuyerSettlementRatios: s.ratios("90peach:2fig"), + }, + expFees: marketFees{marketID: 1, buyerRatio: "90peach:2fig,100peach:1grape"}, + }, + { + name: "buyer ratio: complex", + // Remove one with wrong amounts and don't replace it (10plum:3fig) + // Remove and replace one to change amounts (100peach:3fig -> 90peach:2fig) + // Add one with existing denoms and different amounts (110peach:2grape stomping on 100peach:1grape) + // Add one with same price denom (100peach:1honeydew) + // Add one with same fee denom (100pear:3fig) + // Add one all new (100papaya:5guava) + // Leave one untouched (100prune:2fig) + setup: func() { + store := s.getStore() + keeper.SetBuyerSettlementRatios(store, 1, s.ratios("100peach:3fig,100peach:1grape")) + keeper.SetBuyerSettlementRatios(store, 11, s.ratios("20plum:2fig,100peach:3fig,100peach:1grape,100prune:2fig")) + keeper.SetBuyerSettlementRatios(store, 111, s.ratios("100peach:3fig,100peach:1grape")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 11, + RemoveFeeBuyerSettlementRatios: s.ratios("10plum:3fig,100peach:3fig"), + AddFeeBuyerSettlementRatios: s.ratios("90peach:2fig,110peach:2grape,100peach:1honeydew,100pear:3fig,100papaya:5guava"), + }, + expFees: marketFees{ + marketID: 11, + buyerRatio: "100papaya:5guava,90peach:2fig,110peach:2grape,100peach:1honeydew,100pear:3fig,100prune:2fig", + }, + expNoChange: nil, + expPanic: "", + }, + + // + { + name: "a little bit of everything", + // For each type, add one, replace one, remove one, leave one. + setup: func() { + store := s.getStore() + keeper.SetCreateAskFlatFees(store, 1, s.coins("10grape")) + keeper.SetCreateBidFlatFees(store, 1, s.coins("11guava")) + keeper.SetSellerSettlementFlatFees(store, 1, s.coins("12grapefruit")) + keeper.SetBuyerSettlementFlatFees(store, 1, s.coins("13gooseberry")) + keeper.SetSellerSettlementRatios(store, 1, s.ratios("100papaya:3goumi")) + keeper.SetBuyerSettlementRatios(store, 1, s.ratios("120pineapple:1guarana")) + + keeper.SetCreateAskFlatFees(store, 2, s.coins("201acai,202apple,203apricot")) + keeper.SetCreateBidFlatFees(store, 2, s.coins("211banana,212biriba,212blueberry")) + keeper.SetSellerSettlementFlatFees(store, 2, s.coins("221cactus,222cantaloupe,223cherry")) + keeper.SetBuyerSettlementFlatFees(store, 2, s.coins("231date,232dewberry,233durian")) + keeper.SetSellerSettlementRatios(store, 2, s.ratios("241tangerine:1lemon,242tangerine:2lime,243tayberry:3lime")) + keeper.SetBuyerSettlementRatios(store, 2, s.ratios("251mandarin:4nectarine,252mango:5nectarine,253mango:6nutmeg")) + + keeper.SetCreateAskFlatFees(store, 3, s.coins("30grape")) + keeper.SetCreateBidFlatFees(store, 3, s.coins("31guava")) + keeper.SetSellerSettlementFlatFees(store, 3, s.coins("32grapefruit")) + keeper.SetBuyerSettlementFlatFees(store, 3, s.coins("33gooseberry")) + keeper.SetSellerSettlementRatios(store, 3, s.ratios("300papaya:3goumi")) + keeper.SetBuyerSettlementRatios(store, 3, s.ratios("320pineapple:1guarana")) + }, + msg: &exchange.MsgGovManageFeesRequest{ + MarketId: 2, + AddFeeCreateAskFlat: s.coins("2002apple,204avocado"), + RemoveFeeCreateAskFlat: s.coins("202apple,203apricot"), + AddFeeCreateBidFlat: s.coins("214barbadine,2102blueberry"), + RemoveFeeCreateBidFlat: s.coins("211banana,212blueberry"), + AddFeeSellerSettlementFlat: s.coins("224cassaba,2201cactus"), + RemoveFeeSellerSettlementFlat: s.coins("221cactus,222cantaloupe"), + AddFeeBuyerSettlementFlat: s.coins("2302dewberry,234dragonfruit"), + RemoveFeeBuyerSettlementFlat: s.coins("231date,232dewberry"), + AddFeeSellerSettlementRatios: s.ratios("2402tangerine:20lime,244tamarillo:4lemon"), + RemoveFeeSellerSettlementRatios: s.ratios("241tangerine:1lemon,242tangerine:2lime"), + AddFeeBuyerSettlementRatios: s.ratios("2502mango:50nectarine,254marula:7neem"), + RemoveFeeBuyerSettlementRatios: s.ratios("252mango:5nectarine,253mango:6nutmeg"), + }, + expFees: marketFees{ + marketID: 2, + createAsk: "201acai,2002apple,204avocado", + createBid: "214barbadine,212biriba,2102blueberry", + sellerFlat: "2201cactus,224cassaba,223cherry", + buyerFlat: "2302dewberry,234dragonfruit,233durian", + sellerRatio: "244tamarillo:4lemon,2402tangerine:20lime,243tayberry:3lime", + buyerRatio: "251mandarin:4nectarine,2502mango:50nectarine,254marula:7neem", + }, + expNoChange: []uint32{1, 3}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + origMarketFees := make([]marketFees, len(tc.expNoChange)) + for i, marketID := range tc.expNoChange { + origMarketFees[i] = getMarketFees(marketID) + } + + var expectedEvents sdk.Events + if tc.msg != nil { + event := exchange.NewEventMarketFeesUpdated(tc.msg.MarketId) + expectedEvents = append(expectedEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + testFunc := func() { + s.k.UpdateFees(ctx, tc.msg) + } + s.requirePanicEquals(testFunc, tc.expPanic, "UpdateFees") + if len(tc.expPanic) > 0 || tc.msg == nil { + return + } + + updatedMarketFees := getMarketFees(tc.msg.MarketId) + s.Assert().Equal(tc.expFees, updatedMarketFees, "fees of updated market %d", tc.msg.MarketId) + for _, expected := range origMarketFees { + actual := getMarketFees(expected.marketID) + s.Assert().Equal(expected, actual, "fees of market %d (that should not have changed)", expected.marketID) + } + + actualEvents := em.Events() + s.assertEqualEvents(expectedEvents, actualEvents, "events emitted during UpdateFees") + }) + } +} + +func (s *TestSuite) TestKeeper_IsMarketKnown() { + tests := []struct { + name string + setup func() + marketID uint32 + expected bool + }{ + { + name: "empty state", + setup: nil, + marketID: 1, + expected: false, + }, + { + name: "unknown market id", + setup: func() { + store := s.getStore() + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: false, + }, + { + name: "market known", + setup: func() { + store := s.getStore() + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual bool + testFunc := func() { + actual = s.k.IsMarketKnown(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "IsMarketKnown(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "IsMarketKnown(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_IsMarketActive() { + tests := []struct { + name string + setup func() + marketID uint32 + expected bool + }{ + { + name: "empty state", + setup: nil, + marketID: 1, + expected: false, + }, + { + name: "unknown market id", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: false, + }, + { + name: "market not active", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 2, false) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: false, + }, + { + name: "market active and known", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 2, true) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: true, + }, + { + name: "market inactive but known", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 2, false) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + }, + marketID: 2, + expected: false, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual bool + testFunc := func() { + actual = s.k.IsMarketActive(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "IsMarketActive(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "IsMarketActive(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdateMarketActive() { + tests := []struct { + name string + setup func() + marketID uint32 + active bool + updatedBy string + expErr string + }{ + { + name: "empty state to active", + marketID: 1, + active: true, + updatedBy: "updatedBy___________", + expErr: "market 1 already has accepting-orders true", + }, + { + name: "empty state to inactive", + marketID: 1, + active: false, + updatedBy: "updatedBy___________", + expErr: "", + }, + { + name: "active to active", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 2, false) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketActive(store, 4, true) + keeper.SetMarketActive(store, 5, false) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + keeper.SetMarketKnown(store, 4) + keeper.SetMarketKnown(store, 5) + }, + marketID: 3, + active: true, + updatedBy: "updatedBy___________", + expErr: "market 3 already has accepting-orders true", + }, + { + name: "active to inactive", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 1, true) + keeper.SetMarketActive(store, 2, false) + keeper.SetMarketActive(store, 3, true) + keeper.SetMarketActive(store, 4, true) + keeper.SetMarketActive(store, 5, false) + keeper.SetMarketKnown(store, 1) + keeper.SetMarketKnown(store, 2) + keeper.SetMarketKnown(store, 3) + keeper.SetMarketKnown(store, 4) + keeper.SetMarketKnown(store, 5) + }, + marketID: 3, + active: false, + updatedBy: "updated_by__________", + expErr: "", + }, + { + name: "inactive to active", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 11, true) + keeper.SetMarketActive(store, 12, false) + keeper.SetMarketActive(store, 13, false) + keeper.SetMarketActive(store, 14, true) + keeper.SetMarketActive(store, 15, false) + keeper.SetMarketKnown(store, 11) + keeper.SetMarketKnown(store, 12) + keeper.SetMarketKnown(store, 13) + keeper.SetMarketKnown(store, 14) + keeper.SetMarketKnown(store, 15) + }, + marketID: 13, + active: true, + updatedBy: "updated___by________", + expErr: "", + }, + { + name: "inactive to inactive", + setup: func() { + store := s.getStore() + keeper.SetMarketActive(store, 11, true) + keeper.SetMarketActive(store, 12, false) + keeper.SetMarketActive(store, 13, false) + keeper.SetMarketActive(store, 14, true) + keeper.SetMarketActive(store, 15, false) + keeper.SetMarketKnown(store, 11) + keeper.SetMarketKnown(store, 12) + keeper.SetMarketKnown(store, 13) + keeper.SetMarketKnown(store, 14) + keeper.SetMarketKnown(store, 15) + }, + marketID: 13, + active: false, + updatedBy: "__updated_____by____", + expErr: "market 13 already has accepting-orders false", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expEvents sdk.Events + if len(tc.expErr) == 0 { + event := exchange.NewEventMarketActiveUpdated(tc.marketID, tc.updatedBy, tc.active) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = s.k.UpdateMarketActive(ctx, tc.marketID, tc.active, tc.updatedBy) + } + s.Require().NotPanics(testFunc, "UpdateMarketActive(%d, %t, %s)", tc.marketID, tc.active, string(tc.updatedBy)) + s.assertErrorValue(err, tc.expErr, "UpdateMarketActive(%d, %t, %s)", tc.marketID, tc.active, string(tc.updatedBy)) + + events := em.Events() + s.assertEqualEvents(expEvents, events, "events after UpdateMarketActive") + + if len(tc.expErr) == 0 { + isActive := s.k.IsMarketActive(s.ctx, tc.marketID) + s.Assert().Equal(tc.active, isActive, "IsMarketActive(%d) after UpdateMarketActive(%d, %t, ...)", + tc.marketID, tc.marketID, tc.active) + } + }) + } +} + +func (s *TestSuite) TestKeeper_IsUserSettlementAllowed() { + tests := []struct { + name string + setup func() + marketID uint32 + expected bool + }{ + { + name: "empty state", + marketID: 1, + expected: false, + }, + { + name: "unknown market id", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 1, true) + keeper.SetUserSettlementAllowed(store, 3, true) + }, + marketID: 2, + expected: false, + }, + { + name: "not allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 1, true) + keeper.SetUserSettlementAllowed(store, 2, false) + keeper.SetUserSettlementAllowed(store, 3, true) + }, + marketID: 2, + expected: false, + }, + { + name: "allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 1, true) + keeper.SetUserSettlementAllowed(store, 2, true) + keeper.SetUserSettlementAllowed(store, 3, true) + }, + marketID: 2, + expected: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual bool + testFunc := func() { + actual = s.k.IsUserSettlementAllowed(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "IsUserSettlementAllowed(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "IsUserSettlementAllowed(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdateUserSettlementAllowed() { + tests := []struct { + name string + setup func() + marketID uint32 + allow bool + updatedBy string + expErr string + }{ + { + name: "empty state to allowed", + marketID: 1, + allow: true, + updatedBy: "updatedBy___________", + expErr: "", + }, + { + name: "empty state to not allowed", + marketID: 1, + allow: false, + updatedBy: "updatedBy___________", + expErr: "market 1 already has allow-user-settlement false", + }, + { + name: "allowed to allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 1, true) + keeper.SetUserSettlementAllowed(store, 2, false) + keeper.SetUserSettlementAllowed(store, 3, true) + keeper.SetUserSettlementAllowed(store, 4, true) + keeper.SetUserSettlementAllowed(store, 5, false) + }, + marketID: 3, + allow: true, + updatedBy: "updatedBy___________", + expErr: "market 3 already has allow-user-settlement true", + }, + { + name: "allowed to not allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 1, true) + keeper.SetUserSettlementAllowed(store, 2, false) + keeper.SetUserSettlementAllowed(store, 3, true) + keeper.SetUserSettlementAllowed(store, 4, true) + keeper.SetUserSettlementAllowed(store, 5, false) + }, + marketID: 3, + allow: false, + updatedBy: "updated_by__________", + expErr: "", + }, + { + name: "not allowed to allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 11, true) + keeper.SetUserSettlementAllowed(store, 12, false) + keeper.SetUserSettlementAllowed(store, 13, false) + keeper.SetUserSettlementAllowed(store, 14, true) + keeper.SetUserSettlementAllowed(store, 15, false) + }, + marketID: 13, + allow: true, + updatedBy: "updated___by________", + expErr: "", + }, + { + name: "not allowed to not allowed", + setup: func() { + store := s.getStore() + keeper.SetUserSettlementAllowed(store, 11, true) + keeper.SetUserSettlementAllowed(store, 12, false) + keeper.SetUserSettlementAllowed(store, 13, false) + keeper.SetUserSettlementAllowed(store, 14, true) + keeper.SetUserSettlementAllowed(store, 15, false) + }, + marketID: 13, + allow: false, + updatedBy: "__updated_____by____", + expErr: "market 13 already has allow-user-settlement false", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expEvents sdk.Events + if len(tc.expErr) == 0 { + event := exchange.NewEventMarketUserSettleUpdated(tc.marketID, tc.updatedBy, tc.allow) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = s.k.UpdateUserSettlementAllowed(ctx, tc.marketID, tc.allow, tc.updatedBy) + } + s.Require().NotPanics(testFunc, "UpdateUserSettlementAllowed(%d, %t, %s)", tc.marketID, tc.allow, string(tc.updatedBy)) + s.assertErrorValue(err, tc.expErr, "UpdateUserSettlementAllowed(%d, %t, %s)", tc.marketID, tc.allow, string(tc.updatedBy)) + + events := em.Events() + s.assertEqualEvents(expEvents, events, "events after UpdateUserSettlementAllowed") + + if len(tc.expErr) == 0 { + isActive := s.k.IsUserSettlementAllowed(s.ctx, tc.marketID) + s.Assert().Equal(tc.allow, isActive, "IsUserSettlementAllowed(%d) after UpdateUserSettlementAllowed(%d, %t, ...)", + tc.marketID, tc.marketID, tc.allow) + } + }) + } +} + +func (s *TestSuite) TestKeeper_HasPermission() { + goodAcc := sdk.AccAddress("goodAddr____________") + goodAddr := goodAcc.String() + authority := s.k.GetAuthority() + tests := []struct { + name string + setup func() + marketID uint32 + address string + permission exchange.Permission + expected bool + }{ + { + name: "empty state, empty address", + marketID: 1, + address: "", + permission: 1, + expected: false, + }, + { + name: "empty state, bad address", + marketID: 1, + address: "bad address", + permission: 1, + expected: false, + }, + { + name: "empty state, not authority", + marketID: 1, + address: goodAddr, + permission: 1, + expected: false, + }, + { + name: "empty state, is authority", + marketID: 1, + address: authority, + permission: 1, + expected: true, + }, + { + name: "no market perms, empty address", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: "", + permission: 1, + expected: false, + }, + { + name: "no market perms, bad address", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: "bad address", + permission: 1, + expected: false, + }, + { + name: "no market perms, not authority", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: goodAddr, + permission: 1, + expected: false, + }, + { + name: "no market perms, is authority", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: authority, + permission: 1, + expected: true, + }, + { + name: "market with perms, empty address", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: "", + permission: 1, + expected: false, + }, + { + name: "market with perms, bad address", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: "bad addr", + permission: 1, + expected: false, + }, + { + name: "market with perms, unknown address", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: sdk.AccAddress("other_address_______").String(), + permission: 1, + expected: false, + }, + { + name: "market with perms, authority", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: authority, + permission: 1, + expected: true, + }, + { + name: "address has other perms on this market", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, []exchange.Permission{ + exchange.Permission_settle, exchange.Permission_cancel}) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: goodAddr, + permission: exchange.Permission_set_ids, + expected: false, + }, + { + name: "address only has just perm on this market", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, []exchange.Permission{exchange.Permission_withdraw}) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: goodAddr, + permission: exchange.Permission_withdraw, + expected: true, + }, + { + name: "address has all perms on market", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, goodAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, goodAcc, exchange.AllPermissions()) + }, + marketID: 2, + address: goodAddr, + permission: exchange.Permission_permissions, + expected: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual bool + testFunc := func() { + actual = s.k.HasPermission(s.ctx, tc.marketID, tc.address, tc.permission) + } + s.Require().NotPanics(testFunc, "HasPermission(%d, %q, %s)", tc.marketID, tc.address, tc.permission) + s.Assert().Equal(tc.expected, actual, "HasPermission(%d, %q, %s) result", tc.marketID, tc.address, tc.permission) + }) + } +} + +// permChecker is the function signature of a permission checking function, e.g. CanSettleOrders. +type permChecker func(ctx sdk.Context, marketID uint32, address string) bool + +// runPermTest runs a set of tests on a permission checking function, e.g. CanSettleOrders. +func (s *TestSuite) runPermTest(perm exchange.Permission, checker permChecker, name string) { + allPermsAcc := sdk.AccAddress("allPerms____________") + justPermAcc := sdk.AccAddress("justPerm____________") + otherPermsAcc := sdk.AccAddress("otherPerms__________") + noPermsAcc := sdk.AccAddress("noPerms_____________") + authority := s.k.GetAuthority() + + allPerms := exchange.AllPermissions() + otherPerms := make([]exchange.Permission, 0, len(allPermsAcc)-1) + for _, p := range exchange.AllPermissions() { + if p != perm { + otherPerms = append(otherPerms, p) + } + } + + defaultSetup := func() { + store := s.getStore() + keeper.GrantPermissions(store, 10, allPermsAcc, allPerms) + keeper.GrantPermissions(store, 10, justPermAcc, allPerms) + keeper.GrantPermissions(store, 10, otherPermsAcc, allPerms) + keeper.GrantPermissions(store, 10, noPermsAcc, allPerms) + + keeper.GrantPermissions(store, 11, allPermsAcc, allPerms) + keeper.GrantPermissions(store, 11, justPermAcc, []exchange.Permission{perm}) + keeper.GrantPermissions(store, 11, otherPermsAcc, otherPerms) + + keeper.GrantPermissions(store, 12, allPermsAcc, allPerms) + keeper.GrantPermissions(store, 12, justPermAcc, allPerms) + keeper.GrantPermissions(store, 12, otherPermsAcc, allPerms) + keeper.GrantPermissions(store, 12, noPermsAcc, allPerms) + } + + tests := []struct { + name string + setup func() + marketID uint32 + admin string + expected bool + }{ + { + name: "empty state: empty addr", + marketID: 1, + admin: "", + expected: false, + }, + { + name: "empty state: authority", + marketID: 1, + admin: authority, + expected: true, + }, + { + name: "empty state: addr with all perms", + marketID: 1, + admin: allPermsAcc.String(), + expected: false, + }, + { + name: "empty state: addr with just perm", + marketID: 1, + admin: justPermAcc.String(), + expected: false, + }, + { + name: "empty state: addr with all other perms", + marketID: 1, + admin: otherPermsAcc.String(), + expected: false, + }, + { + name: "empty state: addr without any perms", + marketID: 1, + admin: noPermsAcc.String(), + expected: false, + }, + + { + name: "existing market: empty addr", + setup: defaultSetup, + marketID: 11, + admin: "", + expected: false, + }, + { + name: "existing market: authority", + setup: defaultSetup, + marketID: 11, + admin: authority, + expected: true, + }, + { + name: "existing market: addr with all perms", + setup: defaultSetup, + marketID: 11, + admin: allPermsAcc.String(), + expected: true, + }, + { + name: "existing market: addr with just perm", + setup: defaultSetup, + marketID: 11, + admin: justPermAcc.String(), + expected: true, + }, + { + name: "existing market: addr with all other perms", + setup: defaultSetup, + marketID: 11, + admin: otherPermsAcc.String(), + expected: false, + }, + { + name: "existing market: addr without any perms", + setup: defaultSetup, + marketID: 11, + admin: noPermsAcc.String(), + expected: false, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual bool + testFunc := func() { + actual = checker(s.ctx, tc.marketID, tc.admin) + } + s.Require().NotPanics(testFunc, "%s(%d, %q)", name, tc.marketID, tc.admin) + s.Assert().Equal(tc.expected, actual, "%s(%d, %q) result", name, tc.marketID, tc.admin) + }) + } +} + +func (s *TestSuite) TestKeeper_CanSettleOrders() { + s.runPermTest(exchange.Permission_settle, s.k.CanSettleOrders, "CanSettleOrders") +} + +func (s *TestSuite) TestKeeper_CanSetIDs() { + s.runPermTest(exchange.Permission_set_ids, s.k.CanSetIDs, "CanSetIDs") +} + +func (s *TestSuite) TestKeeper_CanCancelOrdersForMarket() { + s.runPermTest(exchange.Permission_cancel, s.k.CanCancelOrdersForMarket, "CanCancelOrdersForMarket") +} + +func (s *TestSuite) TestKeeper_CanWithdrawMarketFunds() { + s.runPermTest(exchange.Permission_withdraw, s.k.CanWithdrawMarketFunds, "CanWithdrawMarketFunds") +} + +func (s *TestSuite) TestKeeper_CanUpdateMarket() { + s.runPermTest(exchange.Permission_update, s.k.CanUpdateMarket, "CanUpdateMarket") +} + +func (s *TestSuite) TestKeeper_CanManagePermissions() { + s.runPermTest(exchange.Permission_permissions, s.k.CanManagePermissions, "CanManagePermissions") +} + +func (s *TestSuite) TestKeeper_CanManageReqAttrs() { + s.runPermTest(exchange.Permission_attributes, s.k.CanManageReqAttrs, "CanManageReqAttrs") +} + +func (s *TestSuite) TestKeeper_GetUserPermissions() { + addrNone := sdk.AccAddress("address_none________") + addrOne := sdk.AccAddress("address_one_________") + addrTwo := sdk.AccAddress("address_two_________") + addrAll := sdk.AccAddress("address_all_________") + addrEven := sdk.AccAddress("address_even________") + addrOdd := sdk.AccAddress("address_odd_________") + + onePerm := []exchange.Permission{exchange.Permission_settle} + twoPerms := []exchange.Permission{exchange.Permission_cancel, exchange.Permission_attributes} + allPerms := exchange.AllPermissions() + evenPerms := make([]exchange.Permission, 0, 1+len(allPerms)/2) + oddPerms := make([]exchange.Permission, 0, 1+len(allPerms)/2) + for _, p := range allPerms { + if p%2 == 0 { + evenPerms = append(evenPerms, p) + } else { + oddPerms = append(oddPerms, p) + } + } + + defaultSetup := func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, addrNone, allPerms) + keeper.GrantPermissions(store, 1, addrOne, allPerms) + keeper.GrantPermissions(store, 1, addrTwo, allPerms) + keeper.GrantPermissions(store, 1, addrAll, allPerms) + keeper.GrantPermissions(store, 1, addrEven, allPerms) + keeper.GrantPermissions(store, 1, addrOdd, allPerms) + + keeper.GrantPermissions(store, 2, addrNone, nil) + keeper.GrantPermissions(store, 2, addrOne, onePerm) + keeper.GrantPermissions(store, 2, addrTwo, twoPerms) + keeper.GrantPermissions(store, 2, addrAll, allPerms) + keeper.GrantPermissions(store, 2, addrEven, evenPerms) + keeper.GrantPermissions(store, 2, addrOdd, oddPerms) + + keeper.GrantPermissions(store, 3, addrNone, allPerms) + keeper.GrantPermissions(store, 3, addrOne, allPerms) + keeper.GrantPermissions(store, 3, addrTwo, allPerms) + keeper.GrantPermissions(store, 3, addrAll, allPerms) + keeper.GrantPermissions(store, 3, addrEven, allPerms) + keeper.GrantPermissions(store, 3, addrOdd, allPerms) + } + + tests := []struct { + name string + setup func() + marketID uint32 + addr sdk.AccAddress + expected []exchange.Permission + expPanic string + }{ + { + name: "nil addr", + marketID: 1, + addr: nil, + expPanic: "empty address not allowed", + }, + { + name: "empty addr", + marketID: 1, + addr: sdk.AccAddress{}, + expPanic: "empty address not allowed", + }, + { + name: "empty state", + marketID: 1, + addr: sdk.AccAddress("some_address________"), + expected: nil, + }, + { + name: "no perms in market", + setup: defaultSetup, + marketID: 2, + addr: addrNone, + expected: nil, + }, + { + name: "one perm in market", + setup: defaultSetup, + marketID: 2, + addr: addrOne, + expected: onePerm, + }, + { + name: "two perms in market", + setup: defaultSetup, + marketID: 2, + addr: addrTwo, + expected: twoPerms, + }, + { + name: "odd perms", + setup: defaultSetup, + marketID: 2, + addr: addrOdd, + expected: oddPerms, + }, + { + name: "even perms", + setup: defaultSetup, + marketID: 2, + addr: addrEven, + expected: evenPerms, + }, + { + name: "all perms", + setup: defaultSetup, + marketID: 2, + addr: addrAll, + expected: allPerms, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual []exchange.Permission + testFunc := func() { + actual = s.k.GetUserPermissions(s.ctx, tc.marketID, tc.addr) + } + s.requirePanicEquals(testFunc, tc.expPanic, "GetUserPermissions(%d, %q)", tc.marketID, string(tc.addr)) + s.Assert().Equal(tc.expected, actual, "GetUserPermissions(%d, %q) result", tc.marketID, string(tc.addr)) + }) + } +} + +func (s *TestSuite) TestKeeper_GetAccessGrants() { + addrNone := sdk.AccAddress("address_none________") + addrOne := sdk.AccAddress("address_one_________") + addrTwo := sdk.AccAddress("address_two_________") + addrAll := sdk.AccAddress("address_all_________") + addrEven := sdk.AccAddress("address_even________") + addrOdd := sdk.AccAddress("address_odd_________") + + onePerm := []exchange.Permission{exchange.Permission_settle} + oneOtherPerm := []exchange.Permission{exchange.Permission_set_ids} + twoPerms := []exchange.Permission{exchange.Permission_cancel, exchange.Permission_attributes} + allPerms := exchange.AllPermissions() + evenPerms := make([]exchange.Permission, 0, 1+len(allPerms)/2) + oddPerms := make([]exchange.Permission, 0, 1+len(allPerms)/2) + for _, p := range allPerms { + if p%2 == 0 { + evenPerms = append(evenPerms, p) + } else { + oddPerms = append(oddPerms, p) + } + } + + defaultSetup := func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, addrNone, allPerms) + keeper.GrantPermissions(store, 1, addrOne, allPerms) + keeper.GrantPermissions(store, 1, addrTwo, allPerms) + keeper.GrantPermissions(store, 1, addrAll, allPerms) + keeper.GrantPermissions(store, 1, addrEven, allPerms) + keeper.GrantPermissions(store, 1, addrOdd, allPerms) + + keeper.GrantPermissions(store, 2, addrOne, oneOtherPerm) + + keeper.GrantPermissions(store, 3, addrNone, nil) + keeper.GrantPermissions(store, 3, addrOne, onePerm) + keeper.GrantPermissions(store, 3, addrTwo, twoPerms) + keeper.GrantPermissions(store, 3, addrAll, allPerms) + keeper.GrantPermissions(store, 3, addrEven, evenPerms) + keeper.GrantPermissions(store, 3, addrOdd, oddPerms) + + keeper.GrantPermissions(store, 4, addrNone, allPerms) + keeper.GrantPermissions(store, 4, addrOne, allPerms) + keeper.GrantPermissions(store, 4, addrTwo, allPerms) + keeper.GrantPermissions(store, 4, addrAll, allPerms) + keeper.GrantPermissions(store, 4, addrEven, allPerms) + keeper.GrantPermissions(store, 4, addrOdd, allPerms) + } + + tests := []struct { + name string + setup func() + marketID uint32 + expected []exchange.AccessGrant + }{ + { + name: "empty state", + marketID: 1, + expected: nil, + }, + { + name: "market without any permissions", + setup: defaultSetup, + marketID: 5, + expected: nil, + }, + { + name: "market with just one permission", + setup: defaultSetup, + marketID: 2, + expected: []exchange.AccessGrant{ + {Address: addrOne.String(), Permissions: oneOtherPerm}, + }, + }, + { + name: "market with several permissions", + setup: defaultSetup, + marketID: 3, + expected: []exchange.AccessGrant{ + {Address: addrAll.String(), Permissions: allPerms}, + {Address: addrEven.String(), Permissions: evenPerms}, + {Address: addrOdd.String(), Permissions: oddPerms}, + {Address: addrOne.String(), Permissions: onePerm}, + {Address: addrTwo.String(), Permissions: twoPerms}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual []exchange.AccessGrant + testFunc := func() { + actual = s.k.GetAccessGrants(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetAccessGrants(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetAccessGrants(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdatePermissions() { + adminAddr := sdk.AccAddress("admin_address_woooo_").String() + oneAcc := sdk.AccAddress("addr_one____________") + oneAddr := oneAcc.String() + twoAcc := sdk.AccAddress("addr_two____________") + twoAddr := twoAcc.String() + + tests := []struct { + name string + setup func() + msg *exchange.MsgMarketManagePermissionsRequest + expErr string + expPanic string + expGrants []exchange.AccessGrant + }{ + { + name: "nil msg", + msg: nil, + expPanic: "runtime error: invalid memory address or nil pointer dereference", + }, + { + name: "invalid revoke-all addr", + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + RevokeAll: []string{"invalid"}, + }, + expPanic: "decoding bech32 failed: invalid bech32 string length 7", + }, + { + name: "invalid to-revoke addr", + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + ToRevoke: []exchange.AccessGrant{{Address: "invalid"}}, + }, + expPanic: "decoding bech32 failed: invalid bech32 string length 7", + }, + { + name: "invalid to-grant addr", + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + ToGrant: []exchange.AccessGrant{{Address: "invalid"}}, + }, + expPanic: "decoding bech32 failed: invalid bech32 string length 7", + }, + { + name: "revoke-all addr without any perms", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, twoAcc, exchange.AllPermissions()) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 1, + RevokeAll: []string{oneAddr}, + }, + expErr: "account " + oneAddr + " does not have any permissions for market 1", + }, + { + name: "to-revoke perm not granted", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, oneAcc, []exchange.Permission{exchange.Permission_update}) + keeper.GrantPermissions(store, 1, twoAcc, exchange.AllPermissions()) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 1, + ToRevoke: []exchange.AccessGrant{ + {Address: oneAddr, Permissions: []exchange.Permission{exchange.Permission_settle}}, + }, + }, + expErr: "account " + oneAddr + " does not have PERMISSION_SETTLE for market 1", + }, + { + name: "to-add perm already granted", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 2, oneAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, twoAcc, []exchange.Permission{exchange.Permission_update}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 2, + ToGrant: []exchange.AccessGrant{ + {Address: twoAddr, Permissions: []exchange.Permission{exchange.Permission_update}}, + }, + }, + expErr: "account " + twoAddr + " already has PERMISSION_UPDATE for market 2", + }, + { + name: "multiple errors", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 3, sdk.AccAddress("bbbbbbbbbbbbbbbbbbbbb"), []exchange.Permission{ + exchange.Permission_attributes}) + keeper.GrantPermissions(store, 3, sdk.AccAddress("dddddddddddddddddddd"), []exchange.Permission{ + exchange.Permission_cancel, exchange.Permission_attributes}) + keeper.GrantPermissions(store, 3, sdk.AccAddress("ffffffffffffffffffff"), []exchange.Permission{ + exchange.Permission_permissions, exchange.Permission_withdraw}) + keeper.GrantPermissions(store, 3, sdk.AccAddress("gggggggggggggggggggg"), []exchange.Permission{ + exchange.Permission_withdraw, exchange.Permission_attributes}) + keeper.GrantPermissions(store, 3, sdk.AccAddress("hhhhhhhhhhhhhhhhhhhh"), []exchange.Permission{ + exchange.Permission_withdraw, exchange.Permission_set_ids}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 3, + RevokeAll: []string{ + sdk.AccAddress("aaaaaaaaaaaaaaaaaaaaa").String(), + sdk.AccAddress("bbbbbbbbbbbbbbbbbbbbb").String(), + sdk.AccAddress("ccccccccccccccccccccc").String(), + }, + ToRevoke: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("dddddddddddddddddddd").String(), + Permissions: []exchange.Permission{exchange.Permission_update, exchange.Permission_cancel}, + }, + { + Address: sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String(), + Permissions: []exchange.Permission{exchange.Permission_set_ids, exchange.Permission_withdraw}, + }, + { + Address: sdk.AccAddress("ffffffffffffffffffff").String(), + Permissions: []exchange.Permission{exchange.Permission_permissions, exchange.Permission_settle}, + }, + }, + ToGrant: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("gggggggggggggggggggg").String(), + Permissions: []exchange.Permission{exchange.Permission_withdraw, exchange.Permission_attributes}, + }, + { + Address: sdk.AccAddress("hhhhhhhhhhhhhhhhhhhh").String(), + Permissions: []exchange.Permission{exchange.Permission_cancel, exchange.Permission_set_ids}, + }, + { + Address: sdk.AccAddress("iiiiiiiiiiiiiiiiiiii").String(), + Permissions: []exchange.Permission{exchange.Permission_update, exchange.Permission_settle}, + }, + }, + }, + expErr: s.joinErrs( + "account "+sdk.AccAddress("aaaaaaaaaaaaaaaaaaaaa").String()+" does not have any permissions for market 3", + "account "+sdk.AccAddress("ccccccccccccccccccccc").String()+" does not have any permissions for market 3", + "account "+sdk.AccAddress("dddddddddddddddddddd").String()+" does not have PERMISSION_UPDATE for market 3", + "account "+sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String()+" does not have PERMISSION_SET_IDS for market 3", + "account "+sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String()+" does not have PERMISSION_WITHDRAW for market 3", + "account "+sdk.AccAddress("ffffffffffffffffffff").String()+" does not have PERMISSION_SETTLE for market 3", + "account "+sdk.AccAddress("gggggggggggggggggggg").String()+" already has PERMISSION_WITHDRAW for market 3", + "account "+sdk.AccAddress("gggggggggggggggggggg").String()+" already has PERMISSION_ATTRIBUTES for market 3", + "account "+sdk.AccAddress("hhhhhhhhhhhhhhhhhhhh").String()+" already has PERMISSION_SET_IDS for market 3", + ), + }, + { + name: "just a revoke all", + setup: func() { + keeper.GrantPermissions(s.getStore(), 5, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 5, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 6, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 6, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 7, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 7, twoAcc, []exchange.Permission{4, 2}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 6, + RevokeAll: []string{twoAddr}, + }, + expGrants: []exchange.AccessGrant{ + {Address: oneAddr, Permissions: []exchange.Permission{3}}, + }, + }, + { + name: "just a to-revoke", + setup: func() { + keeper.GrantPermissions(s.getStore(), 5, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 5, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 6, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 6, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 7, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 7, twoAcc, []exchange.Permission{4, 2}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 6, + ToRevoke: []exchange.AccessGrant{ + {Address: twoAddr, Permissions: []exchange.Permission{2}}, + }, + }, + expGrants: []exchange.AccessGrant{ + {Address: oneAddr, Permissions: []exchange.Permission{3}}, + {Address: twoAddr, Permissions: []exchange.Permission{4}}, + }, + }, + { + name: "just a to-grant", + setup: func() { + keeper.GrantPermissions(s.getStore(), 5, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 5, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 6, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 6, twoAcc, []exchange.Permission{4, 2}) + keeper.GrantPermissions(s.getStore(), 7, oneAcc, []exchange.Permission{3}) + keeper.GrantPermissions(s.getStore(), 7, twoAcc, []exchange.Permission{4, 2}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 6, + ToGrant: []exchange.AccessGrant{{Address: twoAddr, Permissions: []exchange.Permission{1}}}, + }, + expGrants: []exchange.AccessGrant{ + {Address: oneAddr, Permissions: []exchange.Permission{3}}, + {Address: twoAddr, Permissions: []exchange.Permission{1, 2, 4}}, + }, + }, + { + name: "revoke all grant one", + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 1, oneAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 2, oneAcc, exchange.AllPermissions()) + keeper.GrantPermissions(store, 3, oneAcc, exchange.AllPermissions()) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 2, + RevokeAll: []string{oneAddr}, + ToGrant: []exchange.AccessGrant{{Address: oneAddr, Permissions: []exchange.Permission{5}}}, + }, + expGrants: []exchange.AccessGrant{{Address: oneAddr, Permissions: []exchange.Permission{5}}}, + }, + { + name: "revoke one grant different", + setup: func() { + store := s.getStore() + perms := []exchange.Permission{1, 4, 6} + keeper.GrantPermissions(store, 1, oneAcc, perms) + keeper.GrantPermissions(store, 2, oneAcc, perms) + keeper.GrantPermissions(store, 3, oneAcc, perms) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 2, + ToRevoke: []exchange.AccessGrant{{Address: oneAddr, Permissions: []exchange.Permission{4}}}, + ToGrant: []exchange.AccessGrant{{Address: oneAddr, Permissions: []exchange.Permission{5}}}, + }, + expGrants: []exchange.AccessGrant{{Address: oneAddr, Permissions: []exchange.Permission{1, 5, 6}}}, + }, + { + name: "complex", + // revoke two from addr with two + // revoke all from addr with one, regrant all + // revoke one from addr with all + // grant two to new addr + // revoke one from addr with two, replace with another + setup: func() { + store := s.getStore() + keeper.GrantPermissions(store, 33, sdk.AccAddress("aaaaaaaaaaaaaaaaaaaa"), []exchange.Permission{2, 6}) + keeper.GrantPermissions(store, 33, sdk.AccAddress("bbbbbbbbbbbbbbbbbbbb"), []exchange.Permission{1}) + keeper.GrantPermissions(store, 33, sdk.AccAddress("cccccccccccccccccccc"), exchange.AllPermissions()) + keeper.GrantPermissions(store, 33, sdk.AccAddress("eeeeeeeeeeeeeeeeeeee"), []exchange.Permission{7, 3}) + }, + msg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: adminAddr, + MarketId: 33, + RevokeAll: []string{sdk.AccAddress("bbbbbbbbbbbbbbbbbbbb").String()}, + ToRevoke: []exchange.AccessGrant{ + {Address: sdk.AccAddress("aaaaaaaaaaaaaaaaaaaa").String(), Permissions: []exchange.Permission{2, 6}}, + {Address: sdk.AccAddress("cccccccccccccccccccc").String(), Permissions: []exchange.Permission{3}}, + {Address: sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String(), Permissions: []exchange.Permission{3}}, + }, + ToGrant: []exchange.AccessGrant{ + {Address: sdk.AccAddress("bbbbbbbbbbbbbbbbbbbb").String(), Permissions: exchange.AllPermissions()}, + {Address: sdk.AccAddress("dddddddddddddddddddd").String(), Permissions: []exchange.Permission{5, 4}}, + {Address: sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String(), Permissions: []exchange.Permission{6}}, + }, + }, + expGrants: []exchange.AccessGrant{ + {Address: sdk.AccAddress("bbbbbbbbbbbbbbbbbbbb").String(), Permissions: exchange.AllPermissions()}, + {Address: sdk.AccAddress("cccccccccccccccccccc").String(), Permissions: []exchange.Permission{1, 2, 4, 5, 6, 7}}, + {Address: sdk.AccAddress("dddddddddddddddddddd").String(), Permissions: []exchange.Permission{4, 5}}, + {Address: sdk.AccAddress("eeeeeeeeeeeeeeeeeeee").String(), Permissions: []exchange.Permission{6, 7}}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expEvents sdk.Events + if len(tc.expPanic) == 0 && len(tc.expErr) == 0 { + event := exchange.NewEventMarketPermissionsUpdated(tc.msg.MarketId, tc.msg.Admin) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = s.k.UpdatePermissions(ctx, tc.msg) + } + s.requirePanicEquals(testFunc, tc.expPanic, "UpdatePermissions") + if len(tc.expPanic) > 0 { + return + } + + s.assertErrorValue(err, tc.expErr, "UpdatePermissions error") + + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "events emitted during UpdatePermissions") + + if len(tc.expErr) > 0 { + return + } + + actGrants := s.k.GetAccessGrants(ctx, tc.msg.MarketId) + s.Assert().Equal(tc.expGrants, actGrants, "access grants for market %d after UpdatePermissions", tc.msg.MarketId) + }) + } +} + +func (s *TestSuite) TestKeeper_GetReqAttrsAsk() { + setter := keeper.SetReqAttrsAsk + tests := []struct { + name string + setup func() + marketID uint32 + expected []string + }{ + { + name: "empty state", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "market without any", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 3, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 2, []string{"raspberry"}) + setter(store, 3, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 2, + expected: []string{"raspberry"}, + }, + { + name: "market with two", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 2, []string{"raspberry"}) + setter(store, 3, []string{"knee", "elbow"}) + setter(store, 4, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 3, + expected: []string{"knee", "elbow"}, + }, + { + name: "market with three", + setup: func() { + store := s.getStore() + setter(store, 2, []string{"raspberry"}) + setter(store, 33, []string{"knee", "elbow"}) + setter(store, 444, []string{"head", "shoulders", "toes"}) + }, + marketID: 444, + expected: []string{"head", "shoulders", "toes"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual []string + testFunc := func() { + actual = s.k.GetReqAttrsAsk(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetReqAttrsAsk(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetReqAttrsAsk(%d)", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_GetReqAttrsBid() { + setter := keeper.SetReqAttrsBid + tests := []struct { + name string + setup func() + marketID uint32 + expected []string + }{ + { + name: "empty state", + setup: nil, + marketID: 1, + expected: nil, + }, + { + name: "market without any", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 3, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 2, + expected: nil, + }, + { + name: "market with one", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 2, []string{"raspberry"}) + setter(store, 3, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 2, + expected: []string{"raspberry"}, + }, + { + name: "market with two", + setup: func() { + store := s.getStore() + setter(store, 1, []string{"bb.aa", "*.cc.bb.aa", "banana"}) + setter(store, 2, []string{"raspberry"}) + setter(store, 3, []string{"knee", "elbow"}) + setter(store, 4, []string{"yy.zz", "*.xx.yy.zz", "banana"}) + }, + marketID: 3, + expected: []string{"knee", "elbow"}, + }, + { + name: "market with three", + setup: func() { + store := s.getStore() + setter(store, 2, []string{"raspberry"}) + setter(store, 33, []string{"knee", "elbow"}) + setter(store, 444, []string{"head", "shoulders", "toes"}) + }, + marketID: 444, + expected: []string{"head", "shoulders", "toes"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var actual []string + testFunc := func() { + actual = s.k.GetReqAttrsBid(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetReqAttrsBid(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetReqAttrsBid(%d)", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_CanCreateAsk() { + setter := keeper.SetReqAttrsAsk + addr1 := sdk.AccAddress("addr_one____________") + addr2 := sdk.AccAddress("addr_two____________") + addr3 := sdk.AccAddress("addr_three__________") + + tests := []struct { + name string + setup func() + attrKeeper *MockAttributeKeeper + marketID uint32 + addr sdk.AccAddress + expected bool + expGetAttrCall bool + }{ + { + name: "empty state", + marketID: 1, + addr: sdk.AccAddress("empty_state_addr____"), + expected: true, + }, + { + name: "no req attrs, addr without any attributes", + setup: func() { + store := s.getStore() + setter(store, 7, []string{"bb.aa"}) + setter(store, 9, []string{"yy.zz", "*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, nil, ""). + WithGetAllAttributesAddrResult(addr3, []string{"jk.lm.nl", "yy.zz"}, ""), + marketID: 8, + addr: addr2, + expected: true, + }, + { + name: "no req attrs, addr with some attributes", + setup: func() { + store := s.getStore() + setter(store, 7, []string{"bb.aa"}) + setter(store, 9, []string{"yy.zz", "*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"left", "right"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"jk.lm.nl", "yy.zz"}, ""), + marketID: 8, + addr: addr2, + expected: true, + }, + { + name: "error getting attributes", + setup: func() { + setter(s.getStore(), 4, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, nil, "injected test error"), + marketID: 4, + addr: addr1, + expected: false, + expGetAttrCall: true, + }, + { + name: "one req attr, acc has", + setup: func() { + setter(s.getStore(), 88, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "bb.aa", "lm.no"}, ""), + marketID: 88, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr, acc does not have", + setup: func() { + setter(s.getStore(), 88, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "lm.no"}, ""), + marketID: 88, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc has", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "jk.lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc has two that match", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "ab.cd.lm.no", "cc.bb.aa", "jk.lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc does not have", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has neither", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"one.bb.aa"}, ""), + marketID: 123, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has just first", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"one.bb.aa"}, ""), + marketID: 123, + addr: addr3, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has just second", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr3, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has both, same order", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr1, + expected: true, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has both, opposite order", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"two.bb.aa", "one.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr1, + expected: true, + expGetAttrCall: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expCalls AttributeCalls + if tc.expGetAttrCall { + expCalls.GetAllAttributesAddr = append(expCalls.GetAllAttributesAddr, tc.addr) + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + kpr := s.k.WithAttributeKeeper(tc.attrKeeper) + + var actual bool + testFunc := func() { + actual = kpr.CanCreateAsk(s.ctx, tc.marketID, tc.addr) + } + s.Require().NotPanics(testFunc, "CanCreateAsk(%d, %q)", tc.marketID, string(tc.addr)) + s.Assert().Equal(tc.expected, actual, "CanCreateAsk(%d, %q) result", tc.marketID, string(tc.addr)) + s.assertAttributeKeeperCalls(tc.attrKeeper, expCalls, "CanCreateAsk(%d, %q)", tc.marketID, string(tc.addr)) + }) + } +} + +func (s *TestSuite) TestKeeper_CanCreateBid() { + setter := keeper.SetReqAttrsBid + addr1 := sdk.AccAddress("addr_one____________") + addr2 := sdk.AccAddress("addr_two____________") + addr3 := sdk.AccAddress("addr_three__________") + + tests := []struct { + name string + setup func() + attrKeeper *MockAttributeKeeper + marketID uint32 + addr sdk.AccAddress + expected bool + expGetAttrCall bool + }{ + { + name: "empty state", + marketID: 1, + addr: sdk.AccAddress("empty_state_addr____"), + expected: true, + }, + { + name: "no req attrs, addr without any attributes", + setup: func() { + store := s.getStore() + setter(store, 7, []string{"bb.aa"}) + setter(store, 9, []string{"yy.zz", "*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, nil, ""). + WithGetAllAttributesAddrResult(addr3, []string{"jk.lm.nl", "yy.zz"}, ""), + marketID: 8, + addr: addr2, + expected: true, + }, + { + name: "no req attrs, addr with some attributes", + setup: func() { + store := s.getStore() + setter(store, 7, []string{"bb.aa"}) + setter(store, 9, []string{"yy.zz", "*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"left", "right"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"jk.lm.nl", "yy.zz"}, ""), + marketID: 8, + addr: addr2, + expected: true, + }, + { + name: "error getting attributes", + setup: func() { + setter(s.getStore(), 4, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, nil, "injected test error"), + marketID: 4, + addr: addr1, + expected: false, + expGetAttrCall: true, + }, + { + name: "one req attr, acc has", + setup: func() { + setter(s.getStore(), 88, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "bb.aa", "lm.no"}, ""), + marketID: 88, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr, acc does not have", + setup: func() { + setter(s.getStore(), 88, []string{"bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "lm.no"}, ""), + marketID: 88, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc has", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "jk.lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc has two that match", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "ab.cd.lm.no", "cc.bb.aa", "jk.lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: true, + expGetAttrCall: true, + }, + { + name: "one req attr with wildcard, acc does not have", + setup: func() { + setter(s.getStore(), 42, []string{"*.lm.no"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr2, []string{"yy.zz", "cc.bb.aa", "lm.no"}, ""), + marketID: 42, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has neither", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"one.bb.aa"}, ""), + marketID: 123, + addr: addr2, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has just first", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"one.bb.aa"}, ""), + marketID: 123, + addr: addr3, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has just second", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr3, + expected: false, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has both, same order", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"one.bb.aa", "two.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr1, + expected: true, + expGetAttrCall: true, + }, + { + name: "two req attr, acc has both, opposite order", + setup: func() { + setter(s.getStore(), 123, []string{"one.bb.aa", "two.bb.aa"}) + }, + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(addr1, []string{"two.bb.aa", "one.bb.aa"}, ""). + WithGetAllAttributesAddrResult(addr2, []string{"one.yy.zz", "two.yy.zz"}, ""). + WithGetAllAttributesAddrResult(addr3, []string{"two.bb.aa"}, ""), + marketID: 123, + addr: addr1, + expected: true, + expGetAttrCall: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expCalls AttributeCalls + if tc.expGetAttrCall { + expCalls.GetAllAttributesAddr = append(expCalls.GetAllAttributesAddr, tc.addr) + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + kpr := s.k.WithAttributeKeeper(tc.attrKeeper) + + var actual bool + testFunc := func() { + actual = kpr.CanCreateBid(s.ctx, tc.marketID, tc.addr) + } + s.Require().NotPanics(testFunc, "CanCreateBid(%d, %q)", tc.marketID, string(tc.addr)) + s.Assert().Equal(tc.expected, actual, "CanCreateBid(%d, %q) result", tc.marketID, string(tc.addr)) + s.assertAttributeKeeperCalls(tc.attrKeeper, expCalls, "CanCreateBid(%d, %q)", tc.marketID, string(tc.addr)) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdateReqAttrs() { + tests := []struct { + name string + setup func() + msg *exchange.MsgMarketManageReqAttrsRequest + expAsk []string + expBid []string + expErr string + expPanic string + }{ + // panics and errors. + { + name: "nil msg", + setup: nil, + msg: nil, + expPanic: "runtime error: invalid memory address or nil pointer dereference", + }, + { + name: "invalid attrs", + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateAskToAdd: []string{"three-dashes-not-allowed", "this.one.is.okay", "bad,punctuation"}, + CreateAskToRemove: []string{"internal spaces are bad"}, // no error from this. + CreateBidToAdd: []string{"twodashes-notallowed-either", "this.one.is.also.okay", "really*bad,punctuation"}, + CreateBidToRemove: []string{"what&are*you(doing)?"}, // no error from this. + }, + expErr: s.joinErrs( + "invalid attribute \"three-dashes-not-allowed\"", + "invalid attribute \"bad,punctuation\"", + "invalid attribute \"twodashes-notallowed-either\"", + "invalid attribute \"really*bad,punctuation\"", + ), + }, + { + name: "remove create-ask that is not required", + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateAskToRemove: []string{"not.req"}, + }, + expErr: "cannot remove create-ask required attribute \"not.req\": attribute not currently required", + }, + { + name: "remove create-bid that is not required", + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateBidToRemove: []string{"not.req"}, + }, + expErr: "cannot remove create-bid required attribute \"not.req\": attribute not currently required", + }, + { + name: "add create-ask that is already required", + setup: func() { + keeper.SetReqAttrsAsk(s.getStore(), 7, []string{"already.req"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 7, + CreateAskToAdd: []string{"already.req"}, + }, + expErr: "cannot add create-ask required attribute \"already.req\": attribute already required", + }, + { + name: "add create-ask that is already required", + setup: func() { + keeper.SetReqAttrsBid(s.getStore(), 4, []string{"already.req"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 4, + CreateBidToAdd: []string{"already.req"}, + }, + expErr: "cannot add create-bid required attribute \"already.req\": attribute already required", + }, + { + name: "multiple errors", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 3, []string{"one.ask", "two.ask", "three.ask", "four.ask"}) + keeper.SetReqAttrsBid(store, 3, []string{"one.bid", "two.bid", "three.bid", "four.bid"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "addr_str_of_admin", + MarketId: 3, + CreateAskToAdd: []string{"two.ask", "three .ask", "five.ask"}, + CreateAskToRemove: []string{" four .ask", "five.ask", "six . ask"}, + CreateBidToAdd: []string{"two.bid ", " three.bid", "five. bid"}, + CreateBidToRemove: []string{"four. bid ", "five . bid", "six.bid"}, + }, + expErr: s.joinErrs( + "cannot remove create-ask required attribute \"five.ask\": attribute not currently required", + "cannot remove create-ask required attribute \"six.ask\": attribute not currently required", + "cannot add create-ask required attribute \"two.ask\": attribute already required", + "cannot add create-ask required attribute \"three.ask\": attribute already required", + "cannot remove create-bid required attribute \"five.bid\": attribute not currently required", + "cannot remove create-bid required attribute \"six.bid\": attribute not currently required", + "cannot add create-bid required attribute \"two.bid\": attribute already required", + "cannot add create-bid required attribute \"three.bid\": attribute already required", + ), + }, + + // just create-ask manipulation. + { + name: "remove one create-ask from one", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 9, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateAskToRemove: []string{"ask.can.create.bananas"}, + }, + expAsk: nil, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "remove one create-ask from two", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{"ask.can.create.bananas", "also.ask.okay"}) + keeper.SetReqAttrsBid(store, 9, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateAskToRemove: []string{"also.ask.okay"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "remove one create-ask with wildcard", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{ + "ask.can.create.bananas", "one.ask.can.create.bananas", + "*.ask.can.create.bananas", "two.ask.can.create.bananas", + }) + keeper.SetReqAttrsBid(store, 9, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateAskToRemove: []string{"*.ask.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas", "one.ask.can.create.bananas", "two.ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "remove last two create-ask", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 55, []string{"one.ask.can.create.bananas", "two.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 55, []string{"one.bid.can.create.bananas", "two.bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 55, + CreateAskToRemove: []string{"two.ask.can.create.bananas", "one.ask.can.create.bananas"}, + }, + expAsk: nil, + expBid: []string{"one.bid.can.create.bananas", "two.bid.can.create.bananas"}, + }, + { + name: "add one create-ask to empty", + setup: func() { + keeper.SetReqAttrsBid(s.getStore(), 1, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateAskToAdd: []string{"ask.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "add one create-ask to existing", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 1, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 1, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateAskToAdd: []string{"*.ask.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas", "*.ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "remove one, add diff create-ask", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 4, []string{"four.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 4, []string{"four.bid.can.create.bananas"}) + keeper.SetReqAttrsAsk(store, 5, []string{"five.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 5, []string{"five.bid.can.create.bananas"}) + keeper.SetReqAttrsAsk(store, 6, []string{"six.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 6, []string{"six.bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 5, + CreateAskToAdd: []string{"*.ask.can.create.bananas"}, + CreateAskToRemove: []string{"five.ask.can.create.bananas"}, + }, + expAsk: []string{"*.ask.can.create.bananas"}, + expBid: []string{"five.bid.can.create.bananas"}, + }, + + // just create-bid manipulation. + { + name: "remove one create-bid from one", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 9, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateBidToRemove: []string{"bid.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: nil, + }, + { + name: "remove one create-bid from two", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 9, []string{"bid.can.create.bananas", "also.bid.okay"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateBidToRemove: []string{"also.bid.okay"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "remove one create-bid with wildcard", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 9, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 9, []string{ + "bid.can.create.bananas", "one.bid.can.create.bananas", + "*.bid.can.create.bananas", "two.bid.can.create.bananas", + }) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 9, + CreateBidToRemove: []string{"*.bid.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas", "one.bid.can.create.bananas", "two.bid.can.create.bananas"}, + }, + { + name: "remove last two create-bid", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 55, []string{"one.ask.can.create.bananas", "two.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 55, []string{"one.bid.can.create.bananas", "two.bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 55, + CreateBidToRemove: []string{"two.bid.can.create.bananas", "one.bid.can.create.bananas"}, + }, + expAsk: []string{"one.ask.can.create.bananas", "two.ask.can.create.bananas"}, + expBid: nil, + }, + { + name: "add one create-bid to empty", + setup: func() { + keeper.SetReqAttrsAsk(s.getStore(), 1, []string{"ask.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateBidToAdd: []string{"bid.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas"}, + }, + { + name: "add one create-bid to existing", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 1, []string{"ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 1, []string{"bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 1, + CreateBidToAdd: []string{"*.bid.can.create.bananas"}, + }, + expAsk: []string{"ask.can.create.bananas"}, + expBid: []string{"bid.can.create.bananas", "*.bid.can.create.bananas"}, + }, + { + name: "remove one, add diff create-bid", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 4, []string{"four.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 4, []string{"four.bid.can.create.bananas"}) + keeper.SetReqAttrsAsk(store, 5, []string{"five.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 5, []string{"five.bid.can.create.bananas"}) + keeper.SetReqAttrsAsk(store, 6, []string{"six.ask.can.create.bananas"}) + keeper.SetReqAttrsBid(store, 6, []string{"six.bid.can.create.bananas"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_str", + MarketId: 5, + CreateBidToAdd: []string{"*.bid.can.create.bananas"}, + CreateBidToRemove: []string{"five.bid.can.create.bananas"}, + }, + expAsk: []string{"five.ask.can.create.bananas"}, + expBid: []string{"*.bid.can.create.bananas"}, + }, + + // manipulation of both. + { + name: "add and remove two of each", + setup: func() { + store := s.getStore() + keeper.SetReqAttrsAsk(store, 2, []string{"one.ask", "two.ask", "three.ask"}) + keeper.SetReqAttrsBid(store, 2, []string{"one.bid", "two.bid", "three.bid"}) + }, + msg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: "admin_addr_string", + MarketId: 2, + CreateAskToAdd: []string{"*.other", "four.ask"}, + CreateAskToRemove: []string{"one.ask", "three.ask"}, + CreateBidToAdd: []string{"*.other", "five.bid"}, + CreateBidToRemove: []string{"three.bid", "two.bid"}, + }, + expAsk: []string{"two.ask", "*.other", "four.ask"}, + expBid: []string{"one.bid", "*.other", "five.bid"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expEvents sdk.Events + if len(tc.expPanic) == 0 && len(tc.expErr) == 0 { + event := exchange.NewEventMarketReqAttrUpdated(tc.msg.MarketId, tc.msg.Admin) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = s.k.UpdateReqAttrs(ctx, tc.msg) + } + s.requirePanicEquals(testFunc, tc.expPanic, "UpdateReqAttrs") + s.assertErrorValue(err, tc.expErr, "UpdateReqAttrs error") + + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "events emitted during UpdateReqAttrs") + + if len(tc.expPanic) > 0 || len(tc.expErr) > 0 { + return + } + + reqAttrAsk := s.k.GetReqAttrsAsk(s.ctx, tc.msg.MarketId) + reqAttrBid := s.k.GetReqAttrsBid(s.ctx, tc.msg.MarketId) + s.Assert().Equal(tc.expAsk, reqAttrAsk, "create-ask req attrs after UpdateReqAttrs") + s.Assert().Equal(tc.expBid, reqAttrBid, "create-bid req attrs after UpdateReqAttrs") + }) + } +} + +func (s *TestSuite) TestKeeper_GetMarketAccount() { + baseAcc := func(marketID uint32) *authtypes.BaseAccount { + return &authtypes.BaseAccount{ + Address: exchange.GetMarketAddress(marketID).String(), + PubKey: nil, + AccountNumber: uint64(marketID), + Sequence: uint64(marketID) * 2, + } + } + marketAcc := func(marketID uint32) *exchange.MarketAccount { + return &exchange.MarketAccount{ + BaseAccount: baseAcc(marketID), + MarketId: marketID, + MarketDetails: exchange.MarketDetails{ + Name: fmt.Sprintf("market %d name", marketID), + Description: fmt.Sprintf("This is a description of market %d. It's not very helpful.", marketID), + WebsiteUrl: fmt.Sprintf("https://example.com/market/%d", marketID), + IconUri: fmt.Sprintf("https://icon.example.com/market/%d/small", marketID), + }, + } + } + + tests := []struct { + name string + accKeeper *MockAccountKeeper + marketID uint32 + expected *exchange.MarketAccount + }{ + { + name: "no account for market", + accKeeper: NewMockAccountKeeper(). + WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)). + WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3)), + marketID: 2, + expected: nil, + }, + { + name: "not a market account", + accKeeper: NewMockAccountKeeper(). + WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)). + WithGetAccountResult(exchange.GetMarketAddress(2), baseAcc(2)). + WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3)), + marketID: 2, + expected: nil, + }, + { + name: "market account 1", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)), + marketID: 1, + expected: marketAcc(1), + }, + { + name: "market account 65,536", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(65_536), marketAcc(65_536)), + marketID: 65_536, + expected: marketAcc(65_536), + }, + { + name: "market account max uint32", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(4_294_967_295), marketAcc(4_294_967_295)), + marketID: 4_294_967_295, + expected: marketAcc(4_294_967_295), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + expCalls := AccountCalls{GetAccount: []sdk.AccAddress{exchange.GetMarketAddress(tc.marketID)}} + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + var actual *exchange.MarketAccount + testFunc := func() { + actual = kpr.GetMarketAccount(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetMarketAccount(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetMarketAccount(%d) result", tc.marketID) + s.assertAccountKeeperCalls(tc.accKeeper, expCalls, "GetMarketAccount(%d)", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_GetMarketDetails() { + baseAcc := func(marketID uint32) *authtypes.BaseAccount { + return &authtypes.BaseAccount{ + Address: exchange.GetMarketAddress(marketID).String(), + PubKey: nil, + AccountNumber: uint64(marketID), + Sequence: uint64(marketID) * 2, + } + } + marketDeets := func(marketID uint32) *exchange.MarketDetails { + return &exchange.MarketDetails{ + Name: fmt.Sprintf("market %d name", marketID), + Description: fmt.Sprintf("This is a description of market %d. It's not very helpful.", marketID), + WebsiteUrl: fmt.Sprintf("https://example.com/market/%d", marketID), + IconUri: fmt.Sprintf("https://icon.example.com/market/%d/small", marketID), + } + } + marketAcc := func(marketID uint32) *exchange.MarketAccount { + return &exchange.MarketAccount{ + BaseAccount: baseAcc(marketID), + MarketId: marketID, + MarketDetails: *marketDeets(marketID), + } + } + + tests := []struct { + name string + accKeeper *MockAccountKeeper + marketID uint32 + expected *exchange.MarketDetails + }{ + { + name: "no account for market", + accKeeper: NewMockAccountKeeper(). + WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)). + WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3)), + marketID: 2, + expected: nil, + }, + { + name: "not a market account", + accKeeper: NewMockAccountKeeper(). + WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)). + WithGetAccountResult(exchange.GetMarketAddress(2), baseAcc(2)). + WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3)), + marketID: 2, + expected: nil, + }, + { + name: "market account 1", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1)), + marketID: 1, + expected: marketDeets(1), + }, + { + name: "market account 65,536", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(65_536), marketAcc(65_536)), + marketID: 65_536, + expected: marketDeets(65_536), + }, + { + name: "market account max uint32", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(4_294_967_295), marketAcc(4_294_967_295)), + marketID: 4_294_967_295, + expected: marketDeets(4_294_967_295), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + expCalls := AccountCalls{GetAccount: []sdk.AccAddress{exchange.GetMarketAddress(tc.marketID)}} + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + var actual *exchange.MarketDetails + testFunc := func() { + actual = kpr.GetMarketDetails(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetMarketDetails(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetMarketDetails(%d) result", tc.marketID) + s.assertAccountKeeperCalls(tc.accKeeper, expCalls, "GetMarketDetails(%d)", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_UpdateMarketDetails() { + baseAcc := func(marketID uint32) *authtypes.BaseAccount { + return &authtypes.BaseAccount{ + Address: exchange.GetMarketAddress(marketID).String(), + PubKey: nil, + AccountNumber: uint64(marketID), + Sequence: uint64(marketID) * 2, + } + } + standardDeets := func(marketID uint32) exchange.MarketDetails { + return exchange.MarketDetails{ + Name: fmt.Sprintf("market %d name", marketID), + Description: fmt.Sprintf("This is a description of market %d. It's not very helpful.", marketID), + WebsiteUrl: fmt.Sprintf("https://example.com/market/%d", marketID), + IconUri: fmt.Sprintf("https://icon.example.com/market/%d/small", marketID), + } + } + marketAcc := func(marketID uint32, marketDeets exchange.MarketDetails) *exchange.MarketAccount { + return &exchange.MarketAccount{ + BaseAccount: baseAcc(marketID), + MarketId: marketID, + MarketDetails: marketDeets, + } + } + + tests := []struct { + name string + accKeeper *MockAccountKeeper + marketID uint32 + marketDetails exchange.MarketDetails + updatedBy string + expErr string + expGetAccCall bool + expSetAccCall authtypes.AccountI + }{ + { + name: "invalid market details", + marketID: 1, + marketDetails: exchange.MarketDetails{Name: strings.Repeat("v", exchange.MaxName+1)}, + updatedBy: "whatever", + expErr: fmt.Sprintf("name length %d exceeds maximum length of %d", exchange.MaxName+1, exchange.MaxName), + }, + { + name: "no market account", + marketID: 1, + marketDetails: exchange.MarketDetails{Name: "what"}, + updatedBy: "whatever", + expErr: "market 1 account not found", + expGetAccCall: true, + }, + { + name: "not a market account", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(3), baseAcc(3)), + marketID: 3, + marketDetails: exchange.MarketDetails{Name: "ignored"}, + updatedBy: "whatever", + expErr: "market 3 account not found", + expGetAccCall: true, + }, + { + name: "no changes", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3, standardDeets(3))), + marketID: 3, + marketDetails: standardDeets(3), + updatedBy: "whatever", + expErr: "no changes", + expGetAccCall: true, + }, + { + name: "deleting all fields", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(3), marketAcc(3, standardDeets(3))), + marketID: 3, + marketDetails: exchange.MarketDetails{}, + updatedBy: "i_did_this", + expGetAccCall: true, + expSetAccCall: marketAcc(3, exchange.MarketDetails{}), + }, + { + name: "setting all fields", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(5), marketAcc(5, exchange.MarketDetails{})), + marketID: 5, + marketDetails: standardDeets(5), + updatedBy: "changeling", + expGetAccCall: true, + expSetAccCall: marketAcc(5, standardDeets(5)), + }, + { + name: "changing all fields", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(1), marketAcc(1, standardDeets(1))), + marketID: 1, + marketDetails: standardDeets(12345), + updatedBy: "evil_laugh", + expGetAccCall: true, + expSetAccCall: marketAcc(1, standardDeets(12345)), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var expEvents sdk.Events + var expCalls AccountCalls + if tc.expGetAccCall { + expCalls.GetAccount = append(expCalls.GetAccount, exchange.GetMarketAddress(tc.marketID)) + } + if tc.expSetAccCall != nil { + expCalls.SetAccount = append(expCalls.SetAccount, tc.expSetAccCall) + event := exchange.NewEventMarketDetailsUpdated(tc.marketID, tc.updatedBy) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = kpr.UpdateMarketDetails(ctx, tc.marketID, tc.marketDetails, tc.updatedBy) + } + s.Require().NotPanics(testFunc, "UpdateMarketDetails(%d, ...)", tc.marketDetails) + s.assertErrorValue(err, tc.expErr, "UpdateMarketDetails(%d, ...) error", tc.marketDetails) + s.assertAccountKeeperCalls(tc.accKeeper, expCalls, "UpdateMarketDetails(%d, ...)", tc.marketDetails) + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "events after UpdateMarketDetails(%d, ...)", tc.marketDetails) + }) + } +} + +func (s *TestSuite) TestKeeper_CreateMarket() { + setAccNum := func(id uint64) AccountModifier { + return func(acc authtypes.AccountI) authtypes.AccountI { + err := acc.SetAccountNumber(id) + s.Require().NoError(err, "SetAccountNumber(%d)", id) + return acc + } + } + + tests := []struct { + name string + setup func() + accKeeper *MockAccountKeeper + newAccModifier AccountModifier + market exchange.Market + expMarketID uint32 + expErr string + expHasAccCall bool + expLastAutoID uint32 + }{ + { + name: "market has errors", + market: exchange.Market{ + ReqAttrCreateAsk: []string{"not$money"}, + ReqAttrCreateBid: []string{"no spaces"}, + MarketDetails: exchange.MarketDetails{ + Description: strings.Repeat("w", 1+exchange.MaxDescription), + }, + }, + expErr: s.joinErrs( + "invalid attribute \"not$money\"", + "invalid attribute \"no spaces\"", + "description length 2001 exceeds maximum length of 2000", + ), + }, + { + name: "market address already exists", + accKeeper: NewMockAccountKeeper().WithHasAccountResult(exchange.GetMarketAddress(1), true), + market: exchange.Market{MarketId: 1}, + expErr: "market id 1 account " + exchange.GetMarketAddress(1).String() + " already exists", + expHasAccCall: true, + }, + { + name: "no market id, empty state", + setup: nil, + newAccModifier: setAccNum(88), + market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "Empty Market"}}, + expMarketID: 1, + expHasAccCall: true, + expLastAutoID: 1, + }, + { + name: "no market id, last one was 55", + setup: func() { + keeper.SetLastAutoMarketID(s.getStore(), 55) + }, + newAccModifier: setAccNum(123), + market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "NAME", Description: "DESCRIPTION"}}, + expMarketID: 56, + expHasAccCall: true, + expLastAutoID: 56, + }, + { + name: "market id 78, last one was 22", + setup: func() { + keeper.SetLastAutoMarketID(s.getStore(), 22) + }, + newAccModifier: setAccNum(5), + market: exchange.Market{MarketId: 78}, + expMarketID: 78, + expHasAccCall: true, + expLastAutoID: 22, + }, + { + name: "market id 5, last one was 18", + setup: func() { + keeper.SetLastAutoMarketID(s.getStore(), 18) + }, + newAccModifier: setAccNum(99), + market: exchange.Market{MarketId: 5}, + expMarketID: 5, + expHasAccCall: true, + expLastAutoID: 18, + }, + { + name: "fully filled market", + newAccModifier: setAccNum(324), + market: exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "The third market.", + WebsiteUrl: "https://example.com/market/3/info", + IconUri: "https://icon.example.com/market/3/small", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("incaberry", 88)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 77)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("grape", 66)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("plum", 100), Fee: sdk.NewInt64Coin("jackfruit", 3)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("honeydew", 55)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 500), Fee: sdk.NewInt64Coin("kiwi", 33)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("just_some_address___").String(), + Permissions: exchange.AllPermissions(), + }, + }, + ReqAttrCreateAsk: []string{"*.ask.whatever"}, + ReqAttrCreateBid: []string{"*.bid.whatever"}, + }, + expMarketID: 3, + expHasAccCall: true, + expLastAutoID: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + origMarket := s.copyMarket(tc.market) + var expEvents sdk.Events + var expCalls AccountCalls + if tc.expHasAccCall { + id := tc.expMarketID + if id == 0 { + id = tc.market.MarketId + } + expCalls.HasAccount = append(expCalls.HasAccount, exchange.GetMarketAddress(id)) + } + if tc.newAccModifier != nil { + marketAddr := exchange.GetMarketAddress(tc.expMarketID) + tc.accKeeper.WithNewAccountModifier(marketAddr, tc.newAccModifier) + + expMarketAcc := tc.newAccModifier(&exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{Address: marketAddr.String()}, + MarketId: tc.expMarketID, + MarketDetails: tc.market.MarketDetails, + }) + // Even though the account number isn't set when the account is provided to NewAccount, + // It's all passed by reference. So the arg recorded in the NewAccount call gets updated too. + expCalls.NewAccount = append(expCalls.NewAccount, expMarketAcc) + expCalls.SetAccount = append(expCalls.SetAccount, expMarketAcc) + + event := exchange.NewEventMarketCreated(tc.expMarketID) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var marketID uint32 + var err error + testFunc := func() { + marketID, err = kpr.CreateMarket(ctx, tc.market) + } + s.Require().NotPanics(testFunc, "CreateMarket") + s.assertErrorValue(err, tc.expErr, "CreateMarket error") + s.Assert().Equal(int(tc.expMarketID), int(marketID), "CreateMarket market id") + s.assertAccountKeeperCalls(tc.accKeeper, expCalls, "CreateMarket") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "events emitted during CreateMarket") + s.Assert().Equal(origMarket, tc.market, "market arg after CreateMarket") + + if len(tc.expErr) > 0 || s.T().Failed() { + return + } + + expMarket := tc.market + expMarket.MarketId = marketID + market := kpr.GetMarket(s.ctx, marketID) + s.Assert().Equal(&expMarket, market, "market read from state after CreateMarket") + + lastMarketID := keeper.GetLastAutoMarketID(s.getStore()) + s.Assert().Equal(int(tc.expLastAutoID), int(lastMarketID), "last auto-market id after CreateMarket") + }) + } +} + +func (s *TestSuite) TestKeeper_GetMarket() { + tests := []struct { + name string + accKeeper *MockAccountKeeper + setup func() *exchange.Market // Should return the expected market. + marketID uint32 + }{ + { + name: "unknown market", + marketID: 5, + }, + { + name: "empty market", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(55), &exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{ + Address: exchange.GetMarketAddress(55).String(), + PubKey: nil, + AccountNumber: 71, + Sequence: 0, + }, + MarketId: 55, + MarketDetails: exchange.MarketDetails{}, + }), + setup: func() *exchange.Market { + market := exchange.Market{ + MarketId: 55, + AcceptingOrders: true, + } + keeper.StoreMarket(s.getStore(), market) + return &market + }, + marketID: 55, + }, + { + name: "market without an account", + setup: func() *exchange.Market { + market := exchange.Market{ + MarketId: 71, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: s.addr4.String(), + Permissions: exchange.AllPermissions(), + }, + }, + } + keeper.StoreMarket(s.getStore(), market) + return &market + }, + marketID: 71, + }, + { + name: "market with everything", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(exchange.GetMarketAddress(420), &exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{ + Address: exchange.GetMarketAddress(420).String(), + PubKey: nil, + AccountNumber: 71, + Sequence: 0, + }, + MarketId: 420, + MarketDetails: exchange.MarketDetails{ + Name: "Market 420 name", + Description: "Market 420 description", + WebsiteUrl: "Market 420 url", + IconUri: "Market 420 icon uri", + }, + }), + setup: func() *exchange.Market { + otherMarket1 := exchange.Market{ + MarketId: 419, + AllowUserSettlement: true, + } + otherMarket2 := exchange.Market{ + MarketId: 421, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("whatever", 421)}, + } + expMarket := exchange.Market{ + MarketId: 420, + MarketDetails: exchange.MarketDetails{ + Name: "Market 420 name", + Description: "Market 420 description", + WebsiteUrl: "Market 420 url", + IconUri: "Market 420 icon uri", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("acorn", 6), sdk.NewInt64Coin("apple", 5)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 3), sdk.NewInt64Coin("blueberry", 3)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("farkleberry", 30), sdk.NewInt64Coin("fig", 20)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("pear", 350), Fee: sdk.NewInt64Coin("grape", 7)}, + {Price: sdk.NewInt64Coin("pear", 500), Fee: sdk.NewInt64Coin("grapefruit", 1)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("honeycrisp", 12), sdk.NewInt64Coin("honeydew", 2)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("plum", 377), Fee: sdk.NewInt64Coin("guava", 3)}, + {Price: sdk.NewInt64Coin("prune", 888), Fee: sdk.NewInt64Coin("guava", 5)}, + }, + AcceptingOrders: false, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: s.addr1.String(), + Permissions: []exchange.Permission{ + exchange.Permission_settle, exchange.Permission_set_ids, exchange.Permission_cancel, + }, + }, + { + Address: s.addr2.String(), + Permissions: []exchange.Permission{ + exchange.Permission_update, exchange.Permission_permissions, exchange.Permission_attributes, + }, + }, + { + Address: s.addr3.String(), + Permissions: exchange.AllPermissions(), + }, + { + Address: s.addr4.String(), + Permissions: []exchange.Permission{exchange.Permission_withdraw}, + }, + }, + ReqAttrCreateAsk: []string{"create-ask.my.market", "*.kyc.someone"}, + ReqAttrCreateBid: []string{"create-bid.my.market", "*.kyc.someone"}, + } + + store := s.getStore() + keeper.StoreMarket(store, otherMarket1) + keeper.StoreMarket(store, expMarket) + keeper.StoreMarket(store, otherMarket2) + + return &expMarket + }, + marketID: 420, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + var expMarket *exchange.Market + if tc.setup != nil { + expMarket = tc.setup() + } + + var expCalls AccountCalls + if expMarket != nil { + expCalls.GetAccount = append(expCalls.GetAccount, exchange.GetMarketAddress(tc.marketID)) + } + + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + var actMarket *exchange.Market + testFunc := func() { + actMarket = kpr.GetMarket(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetMarket(%d)", tc.marketID) + s.Assert().Equal(expMarket, actMarket, "GetMarket(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_IterateMarkets() { + var markets []*exchange.Market + stopAfter := func(count int) func(market *exchange.Market) bool { + return func(market *exchange.Market) bool { + markets = append(markets, market) + return len(markets) >= count + } + } + getAll := func(market *exchange.Market) bool { + markets = append(markets, market) + return false + } + + standardDetails := func(marketID uint32) exchange.MarketDetails { + return exchange.MarketDetails{ + Name: fmt.Sprintf("Market %d", marketID), + Description: fmt.Sprintf("Description fo market %d. It's not very informational.", marketID), + WebsiteUrl: fmt.Sprintf("http://example.com/market/%d/info", marketID), + IconUri: fmt.Sprintf("http://example.com/market/%d/icon/huge", marketID), + } + } + standardMarket := func(marketID uint32) *exchange.Market { + return &exchange.Market{ + MarketId: marketID, + MarketDetails: standardDetails(marketID), + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("askflat", int64(marketID))}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("bidflat", int64(marketID))}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("sellerflat", int64(marketID))}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("sellerprice", 500+int64(marketID)), Fee: sdk.NewInt64Coin("sellerfee", int64(marketID))}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("buyerflat", int64(marketID))}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("buyerprice", 1500+int64(marketID)), Fee: sdk.NewInt64Coin("buyerfee", 100+int64(marketID))}, + }, + AcceptingOrders: true, + AllowUserSettlement: false, + AccessGrants: []exchange.AccessGrant{{Address: s.addr5.String(), Permissions: exchange.AllPermissions()}}, + ReqAttrCreateAsk: []string{fmt.Sprintf("%d.ask.create", marketID)}, + ReqAttrCreateBid: []string{fmt.Sprintf("%d.bid.create", marketID)}, + } + } + mustCreateMarket := func(kpr keeper.Keeper, market exchange.Market) { + _, err := kpr.CreateMarket(s.ctx, market) + s.Require().NoError(err, "CreateMarket(%d)", market.MarketId) + } + + tests := []struct { + name string + setup func() keeper.Keeper + cb func(market *exchange.Market) bool + expMarkets []*exchange.Market + }{ + { + name: "empty state", + cb: getAll, + expMarkets: nil, + }, + { + name: "just market 1", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(1)) + return kpr + }, + cb: getAll, + expMarkets: []*exchange.Market{standardMarket(1)}, + }, + { + name: "just market 20", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(20)) + return kpr + }, + cb: getAll, + expMarkets: []*exchange.Market{standardMarket(20)}, + }, + { + name: "markets 1 through 5: get all", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(1)) + mustCreateMarket(kpr, *standardMarket(4)) + mustCreateMarket(kpr, *standardMarket(2)) + mustCreateMarket(kpr, *standardMarket(5)) + mustCreateMarket(kpr, *standardMarket(3)) + return kpr + }, + cb: getAll, + expMarkets: []*exchange.Market{ + standardMarket(1), + standardMarket(2), + standardMarket(3), + standardMarket(4), + standardMarket(5), + }, + }, + { + name: "markets 1 through 5: get first", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(1)) + mustCreateMarket(kpr, *standardMarket(4)) + mustCreateMarket(kpr, *standardMarket(2)) + mustCreateMarket(kpr, *standardMarket(5)) + mustCreateMarket(kpr, *standardMarket(3)) + return kpr + }, + cb: stopAfter(1), + expMarkets: []*exchange.Market{standardMarket(1)}, + }, + { + name: "markets 1 through 5: get three", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(1)) + mustCreateMarket(kpr, *standardMarket(4)) + mustCreateMarket(kpr, *standardMarket(2)) + mustCreateMarket(kpr, *standardMarket(5)) + mustCreateMarket(kpr, *standardMarket(3)) + return kpr + }, + cb: stopAfter(3), + expMarkets: []*exchange.Market{ + standardMarket(1), + standardMarket(2), + standardMarket(3), + }, + }, + { + name: "five randomly numbered markets: get all", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(63)) + mustCreateMarket(kpr, *standardMarket(23)) + mustCreateMarket(kpr, *standardMarket(36)) + mustCreateMarket(kpr, *standardMarket(6)) + mustCreateMarket(kpr, *standardMarket(14)) + return kpr + }, + cb: getAll, + expMarkets: []*exchange.Market{ + standardMarket(6), + standardMarket(14), + standardMarket(23), + standardMarket(36), + standardMarket(63), + }, + }, + { + name: "five randomly numbered markets: get first", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(63)) + mustCreateMarket(kpr, *standardMarket(23)) + mustCreateMarket(kpr, *standardMarket(36)) + mustCreateMarket(kpr, *standardMarket(6)) + mustCreateMarket(kpr, *standardMarket(14)) + return kpr + }, + cb: stopAfter(1), + expMarkets: []*exchange.Market{standardMarket(6)}, + }, + { + name: "five randomly numbered markets: get three", + setup: func() keeper.Keeper { + kpr := s.k.WithAccountKeeper(NewMockAccountKeeper()) + mustCreateMarket(kpr, *standardMarket(63)) + mustCreateMarket(kpr, *standardMarket(23)) + mustCreateMarket(kpr, *standardMarket(36)) + mustCreateMarket(kpr, *standardMarket(6)) + mustCreateMarket(kpr, *standardMarket(14)) + return kpr + }, + cb: stopAfter(3), + expMarkets: []*exchange.Market{ + standardMarket(6), + standardMarket(14), + standardMarket(23), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + kpr := s.k + if tc.setup != nil { + kpr = tc.setup() + } + + markets = nil + testFunc := func() { + kpr.IterateMarkets(s.ctx, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateMarkets") + s.Assert().Equal(tc.expMarkets, markets, "markets iterated") + }) + } +} + +func (s *TestSuite) TestKeeper_GetMarketBrief() { + tests := []struct { + name string + accKeeper *MockAccountKeeper + marketID uint32 + expected *exchange.MarketBrief + }{ + { + name: "no account", + marketID: 1, + expected: nil, + }, + { + name: "empty details", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(s.marketAddr2, &exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{ + Address: s.marketAddr2.String(), + AccountNumber: 777, + }, + MarketId: 2, + MarketDetails: exchange.MarketDetails{}, + }), + marketID: 2, + expected: &exchange.MarketBrief{ + MarketId: 2, + MarketAddress: s.marketAddr2.String(), + MarketDetails: exchange.MarketDetails{}, + }, + }, + { + name: "full details", + accKeeper: NewMockAccountKeeper().WithGetAccountResult(s.marketAddr3, &exchange.MarketAccount{ + BaseAccount: &authtypes.BaseAccount{ + Address: s.marketAddr3.String(), + AccountNumber: 777, + }, + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "Market Three's description is a bit lacking here.", + WebsiteUrl: "website three", + IconUri: "icon three", + }, + }), + marketID: 3, + expected: &exchange.MarketBrief{ + MarketId: 3, + MarketAddress: s.marketAddr3.String(), + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "Market Three's description is a bit lacking here.", + WebsiteUrl: "website three", + IconUri: "icon three", + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.accKeeper == nil { + tc.accKeeper = NewMockAccountKeeper() + } + kpr := s.k.WithAccountKeeper(tc.accKeeper) + + var actual *exchange.MarketBrief + testFunc := func() { + actual = kpr.GetMarketBrief(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "GetMarketBrief(%d)", tc.marketID) + s.Assert().Equal(tc.expected, actual, "GetMarketBrief(%d) result", tc.marketID) + }) + } +} + +func (s *TestSuite) TestKeeper_WithdrawMarketFunds() { + tests := []struct { + name string + bankKeeper *MockBankKeeper + marketID uint32 + toAddr sdk.AccAddress + amount sdk.Coins + withdrawnBy string + expErr string + }{ + { + name: "market 1: error from SendCoins", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("woopsie-daisy: an error story"), + marketID: 1, + toAddr: s.addr1, + amount: sdk.NewCoins(sdk.NewInt64Coin("oops", 55)), + withdrawnBy: "noone", + expErr: "failed to withdraw 55oops from market 1: woopsie-daisy: an error story", + }, + { + name: "market 8: error from SendCoins", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("ouch-ouch-ouch: a sequel error story"), + marketID: 8, + toAddr: s.addr1, + amount: sdk.NewCoins(sdk.NewInt64Coin("awwww", 77), sdk.NewInt64Coin("hurts", 3)), + withdrawnBy: "stillnoone", + expErr: "failed to withdraw 77awwww,3hurts from market 8: ouch-ouch-ouch: a sequel error story", + }, + { + name: "market 1: okay", + marketID: 1, + toAddr: s.addr3, + amount: sdk.NewCoins(sdk.NewInt64Coin("yay", 4444)), + withdrawnBy: "thatoneguy", + }, + { + name: "market 8: okay", + marketID: 8, + toAddr: s.addr5, + amount: sdk.NewCoins(sdk.NewInt64Coin("kaching", 500_000_001)), + withdrawnBy: "itwasallme", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + expCalls := BankCalls{ + SendCoins: []*SendCoinsArgs{ + { + fromAddr: exchange.GetMarketAddress(tc.marketID), + toAddr: tc.toAddr, + amt: tc.amount, + }, + }, + } + + var expEvents sdk.Events + if len(tc.expErr) == 0 { + event := exchange.NewEventMarketWithdraw(tc.marketID, tc.amount, tc.toAddr, tc.withdrawnBy) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + kpr := s.k.WithBankKeeper(tc.bankKeeper) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = kpr.WithdrawMarketFunds(ctx, tc.marketID, tc.toAddr, tc.amount, tc.withdrawnBy) + } + s.Require().NotPanics(testFunc, "WithdrawMarketFunds(%d, %s, %q, %q)", + tc.marketID, s.getAddrName(tc.toAddr), tc.amount, tc.withdrawnBy) + s.assertErrorValue(err, tc.expErr, "WithdrawMarketFunds(%d, %s, %q, %q) error", + tc.marketID, s.getAddrName(tc.toAddr), tc.amount, tc.withdrawnBy) + s.assertBankKeeperCalls(tc.bankKeeper, expCalls, "WithdrawMarketFunds(%d, %s, %q, %q)", + tc.marketID, s.getAddrName(tc.toAddr), tc.amount, tc.withdrawnBy) + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "WithdrawMarketFunds(%d, %s, %q, %q) events", + tc.marketID, s.getAddrName(tc.toAddr), tc.amount, tc.withdrawnBy) + }) + } +} + +func (s *TestSuite) TestKeeper_ValidateMarket() { + noBuyerErr := func(denom string) string { + return "seller settlement fee ratios have price denom \"" + denom + "\" but there are no " + + "buyer settlement fee ratios with that price denom" + } + noSellerErr := func(denom string) string { + return "buyer settlement fee ratios have price denom \"" + denom + "\" but there is not a " + + "seller settlement fee ratio with that price denom" + } + + tests := []struct { + name string + setup func() + marketID uint32 + expErr string + }{ + { + name: "market doesn't exist", + marketID: 1, + expErr: "market 1 does not exist", + }, + { + name: "seller price denom not in buyer", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 1, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("500pear"), Fee: s.coin("3pear")}, + {Price: s.coin("500prune"), Fee: s.coin("2prune")}, + }, + FeeBuyerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500prune"), Fee: s.coin("2fig")}}, + }) + }, + marketID: 1, + expErr: noBuyerErr("pear"), + }, + { + name: "buyer price denom not in seller", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 1, + FeeSellerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500pear"), Fee: s.coin("3pear")}}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("500pear"), Fee: s.coin("1grape")}, + {Price: s.coin("500prune"), Fee: s.coin("2fig")}, + }, + }) + }, + marketID: 1, + expErr: noSellerErr("prune"), + }, + { + name: "multiple errors", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 1, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("600papaya"), Fee: s.coin("1papaya")}, + {Price: s.coin("800peach"), Fee: s.coin("7peach")}, + {Price: s.coin("500pear"), Fee: s.coin("3pear")}, + }, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("800papaya"), Fee: s.coin("3honeydew")}, + {Price: s.coin("500plum"), Fee: s.coin("3fig")}, + {Price: s.coin("600prune"), Fee: s.coin("9grape")}, + }, + }) + }, + marketID: 1, + expErr: s.joinErrs( + noBuyerErr("peach"), noBuyerErr("pear"), + noSellerErr("plum"), noSellerErr("prune"), + ), + }, + { + name: "no ratios", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{MarketId: 2}) + }, + marketID: 2, + expErr: "", + }, + { + name: "no buyer ratios", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 2, + FeeSellerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500pear"), Fee: s.coin("3pear")}}, + }) + }, + marketID: 2, + expErr: "", + }, + { + name: "no seller ratios", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 2, + FeeBuyerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500pear"), Fee: s.coin("3pear")}}, + }) + }, + marketID: 2, + expErr: "", + }, + { + name: "one ratio each, same price denoms", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 2, + FeeSellerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500pear"), Fee: s.coin("3pear")}}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("500pear"), Fee: s.coin("2fig")}}, + }) + }, + marketID: 2, + expErr: "", + }, + { + name: "two seller denoms, four buyer ratios with those denoms", + setup: func() { + keeper.StoreMarket(s.getStore(), exchange.Market{ + MarketId: 55, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("300plum"), Fee: s.coin("1plum")}, + {Price: s.coin("800peach"), Fee: s.coin("77peach")}, + }, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: s.coin("500plum"), Fee: s.coin("3plum")}, + {Price: s.coin("600plum"), Fee: s.coin("2fig")}, + {Price: s.coin("800peach"), Fee: s.coin("78peach")}, + {Price: s.coin("900peach"), Fee: s.coin("6fig")}, + }, + }) + }, + marketID: 55, + expErr: "", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var err error + testFunc := func() { + err = s.k.ValidateMarket(s.ctx, tc.marketID) + } + s.Require().NotPanics(testFunc, "ValidateMarket(%d)", tc.marketID) + s.assertErrorValue(err, tc.expErr, "ValidateMarket(%d) error", tc.marketID) + }) + } +} diff --git a/x/exchange/keeper/mocks_test.go b/x/exchange/keeper/mocks_test.go index 2019b08ac6..95de9161a7 100644 --- a/x/exchange/keeper/mocks_test.go +++ b/x/exchange/keeper/mocks_test.go @@ -3,44 +3,17 @@ package keeper_test import ( "errors" "fmt" - "strings" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/quarantine" + attrtypes "github.com/provenance-io/provenance/x/attribute/types" "github.com/provenance-io/provenance/x/exchange" ) -// toStrings converts a slice to indexed strings using the provided stringer func. -func toStrings[T any](vals []T, stringer func(T) string) []string { - if vals == nil { - return nil - } - rv := make([]string, len(vals)) - for i, val := range vals { - rv[i] = fmt.Sprintf("[%d]:%s", i, stringer(val)) - } - return rv -} - -// assertEqualSlice asserts that expected = actual and returns true if so. -// If not, returns false and the stringer is applied to each entry and the comparison -// is redone on the strings in the hopes that it helps identify the problem. -func assertEqualSlice[T any](s *TestSuite, expected, actual []T, stringer func(T) string, msg string, args ...interface{}) bool { - s.T().Helper() - if s.Assert().Equalf(expected, actual, msg, args...) { - return true - } - // compare each as strings in the hopes that makes it easier to identify the problem. - expStrs := toStrings(expected, stringer) - actStrs := toStrings(actual, stringer) - s.Assert().Equalf(expStrs, actStrs, "strings: "+msg, args...) - return false -} - // ############################################################################# // ############################# ############################# // ########################### MockAccountKeeper ########################### @@ -51,28 +24,38 @@ var _ exchange.AccountKeeper = (*MockAccountKeeper)(nil) // MockAccountKeeper satisfies the exchange.AccountKeeper interface but just records the calls and allows dictation of results. type MockAccountKeeper struct { - Calls AccountCalls - GetAccountResultsMap map[string]authtypes.AccountI - HasAccountResultsMap map[string]bool - NewAccountResultsMap map[string]authtypes.AccountI + Calls AccountCalls + GetAccountResultsMap map[string]authtypes.AccountI + HasAccountResultsMap map[string]bool + NewAccountModifierMap map[string]AccountModifier } // AccountCalls contains all the calls that the mock account keeper makes. type AccountCalls struct { - GetAccountCalls []sdk.AccAddress - SetAccountCalls []authtypes.AccountI - HasAccountCalls []sdk.AccAddress - NewAccountCalls []authtypes.AccountI + GetAccount []sdk.AccAddress + SetAccount []authtypes.AccountI + HasAccount []sdk.AccAddress + NewAccount []authtypes.AccountI } +// AccountModifier is a function that can alter an account. +type AccountModifier func(authtypes.AccountI) authtypes.AccountI + +// NoopAccMod is a no-op AccountModifier. +func NoopAccMod(a authtypes.AccountI) authtypes.AccountI { + return a +} + +var _ AccountModifier = NoopAccMod + // NewMockAccountKeeper creates a new empty MockAccountKeeper. // Follow it up with WithGetAccountResult, WithHasAccountResult, // and/or WithNewAccountResult to dictate results. func NewMockAccountKeeper() *MockAccountKeeper { return &MockAccountKeeper{ - GetAccountResultsMap: make(map[string]authtypes.AccountI), - HasAccountResultsMap: make(map[string]bool), - NewAccountResultsMap: make(map[string]authtypes.AccountI), + GetAccountResultsMap: make(map[string]authtypes.AccountI), + HasAccountResultsMap: make(map[string]bool), + NewAccountModifierMap: make(map[string]AccountModifier), } } @@ -96,13 +79,13 @@ func (k *MockAccountKeeper) WithHasAccountResult(addr sdk.AccAddress, result boo // When NewAccount is called, if the address provided has an entry here, that is returned, // otherwise, the provided AccountI is returned. // This method both updates the receiver and returns it. -func (k *MockAccountKeeper) WithNewAccountResult(result authtypes.AccountI) *MockAccountKeeper { - k.NewAccountResultsMap[string(result.GetAddress())] = result +func (k *MockAccountKeeper) WithNewAccountModifier(addr sdk.AccAddress, modifier AccountModifier) *MockAccountKeeper { + k.NewAccountModifierMap[string(addr)] = modifier return k } func (k *MockAccountKeeper) GetAccount(_ sdk.Context, addr sdk.AccAddress) authtypes.AccountI { - k.Calls.GetAccountCalls = append(k.Calls.GetAccountCalls, addr) + k.Calls.GetAccount = append(k.Calls.GetAccount, addr) if rv, found := k.GetAccountResultsMap[string(addr)]; found { return rv } @@ -110,11 +93,12 @@ func (k *MockAccountKeeper) GetAccount(_ sdk.Context, addr sdk.AccAddress) autht } func (k *MockAccountKeeper) SetAccount(_ sdk.Context, acc authtypes.AccountI) { - k.Calls.SetAccountCalls = append(k.Calls.SetAccountCalls, acc) + k.Calls.SetAccount = append(k.Calls.SetAccount, acc) + k.WithGetAccountResult(acc.GetAddress(), acc) } func (k *MockAccountKeeper) HasAccount(_ sdk.Context, addr sdk.AccAddress) bool { - k.Calls.HasAccountCalls = append(k.Calls.HasAccountCalls, addr) + k.Calls.HasAccount = append(k.Calls.HasAccount, addr) if rv, found := k.HasAccountResultsMap[string(addr)]; found { return rv } @@ -122,48 +106,48 @@ func (k *MockAccountKeeper) HasAccount(_ sdk.Context, addr sdk.AccAddress) bool } func (k *MockAccountKeeper) NewAccount(_ sdk.Context, acc authtypes.AccountI) authtypes.AccountI { - k.Calls.NewAccountCalls = append(k.Calls.NewAccountCalls, acc) - if rv, found := k.NewAccountResultsMap[string(acc.GetAddress())]; found { - return rv + k.Calls.NewAccount = append(k.Calls.NewAccount, acc) + if modifier, found := k.NewAccountModifierMap[string(acc.GetAddress())]; found { + return modifier(acc) } return acc } -// assertGetAccountCalls asserts that a mock keeper's GetAccountCalls match the provided expected calls. +// assertGetAccountCalls asserts that a mock keeper's Calls.GetAccount match the provided expected calls. func (s *TestSuite) assertGetAccountCalls(mk *MockAccountKeeper, expected []sdk.AccAddress, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.GetAccountCalls, s.getAddrName, + return assertEqualSlice(s, expected, mk.Calls.GetAccount, s.getAddrName, msg+" GetAccount calls", args...) } -// assertSetAccountCalls asserts that a mock keeper's SetAccountCalls match the provided expected calls. +// assertSetAccountCalls asserts that a mock keeper's Calls.SetAccount match the provided expected calls. func (s *TestSuite) assertSetAccountCalls(mk *MockAccountKeeper, expected []authtypes.AccountI, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.SetAccountCalls, authtypes.AccountI.String, + return assertEqualSlice(s, expected, mk.Calls.SetAccount, authtypes.AccountI.String, msg+" SetAccount calls", args...) } -// assertHasAccountCalls asserts that a mock keeper's HasAccountCalls match the provided expected calls. +// assertHasAccountCalls asserts that a mock keeper's Calls.HasAccount match the provided expected calls. func (s *TestSuite) assertHasAccountCalls(mk *MockAccountKeeper, expected []sdk.AccAddress, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.HasAccountCalls, s.getAddrName, + return assertEqualSlice(s, expected, mk.Calls.HasAccount, s.getAddrName, msg+" HasAccount calls", args...) } -// assertNewAccountCalls asserts that a mock keeper's NewAccountCalls match the provided expected calls. +// assertNewAccountCalls asserts that a mock keeper's Calls.NewAccount match the provided expected calls. func (s *TestSuite) assertNewAccountCalls(mk *MockAccountKeeper, expected []authtypes.AccountI, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.NewAccountCalls, authtypes.AccountI.String, + return assertEqualSlice(s, expected, mk.Calls.NewAccount, authtypes.AccountI.String, msg+" NewAccount calls", args...) } // assertAccountKeeperCalls asserts that all the calls made to a mock account keeper match the provided expected calls. func (s *TestSuite) assertAccountKeeperCalls(mk *MockAccountKeeper, expected AccountCalls, msg string, args ...interface{}) bool { s.T().Helper() - rv := s.assertGetAccountCalls(mk, expected.GetAccountCalls, msg, args...) - rv = s.assertSetAccountCalls(mk, expected.SetAccountCalls, msg, args...) && rv - rv = s.assertHasAccountCalls(mk, expected.HasAccountCalls, msg, args...) && rv - return s.assertNewAccountCalls(mk, expected.NewAccountCalls, msg, args...) && rv + rv := s.assertGetAccountCalls(mk, expected.GetAccount, msg, args...) + rv = s.assertSetAccountCalls(mk, expected.SetAccount, msg, args...) && rv + rv = s.assertHasAccountCalls(mk, expected.HasAccount, msg, args...) && rv + return s.assertNewAccountCalls(mk, expected.NewAccount, msg, args...) && rv } // ############################################################################# @@ -182,7 +166,7 @@ type MockAttributeKeeper struct { // AttributeCalls contains all the calls that the mock attribute keeper makes. type AttributeCalls struct { - GetAllAttributesAddrCalls [][]byte + GetAllAttributesAddr [][]byte } // GetAllAttributesAddrResult contains the result args to return for a GetAllAttributesAddr call. @@ -202,33 +186,45 @@ func NewMockAttributeKeeper() *MockAttributeKeeper { // WithGetAllAttributesAddrResult sets up the provided address to return the given attrs // and error from calls to GetAllAttributesAddr. An empty string means no error. // This method both updates the receiver and returns it. -func (k *MockAttributeKeeper) WithGetAllAttributesAddrResult(addr []byte, attrs []attrtypes.Attribute, errStr string) *MockAttributeKeeper { +func (k *MockAttributeKeeper) WithGetAllAttributesAddrResult(addr []byte, attrNames []string, errStr string) *MockAttributeKeeper { + var attrs []attrtypes.Attribute + if attrNames != nil { + attrs = make([]attrtypes.Attribute, len(attrNames)) + for i, name := range attrNames { + attrs[i] = attrtypes.Attribute{ + Name: name, + Value: []byte("this is the " + name + " value"), + AttributeType: attrtypes.AttributeType_String, + Address: sdk.AccAddress(addr).String(), + } + } + } k.GetAllAttributesAddrResultsMap[string(addr)] = NewGetAllAttributesAddrResult(attrs, errStr) return k } func (k *MockAttributeKeeper) GetAllAttributesAddr(_ sdk.Context, addr []byte) ([]attrtypes.Attribute, error) { - k.Calls.GetAllAttributesAddrCalls = append(k.Calls.GetAllAttributesAddrCalls, addr) + k.Calls.GetAllAttributesAddr = append(k.Calls.GetAllAttributesAddr, addr) if rv, found := k.GetAllAttributesAddrResultsMap[string(addr)]; found { return rv.attrs, rv.err } return nil, nil } -// assertGetAllAttributesAddrCalls asserts that a mock keeper's GetAllAttributesAddrCalls match the provided expected calls. +// assertGetAllAttributesAddrCalls asserts that a mock keeper's Calls.GetAllAttributesAddr match the provided expected calls. func (s *TestSuite) assertGetAllAttributesAddrCalls(mk *MockAttributeKeeper, expected [][]byte, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.GetAllAttributesAddrCalls, + return assertEqualSlice(s, expected, mk.Calls.GetAllAttributesAddr, func(addr []byte) string { return s.getAddrName(addr) }, - msg+" NewAccount calls", args...) + msg+" GetAllAttributesAddr calls", args...) } // assertAttributeKeeperCalls asserts that all the calls made to a mock account keeper match the provided expected calls. func (s *TestSuite) assertAttributeKeeperCalls(mk *MockAttributeKeeper, expected AttributeCalls, msg string, args ...interface{}) bool { s.T().Helper() - return s.assertGetAllAttributesAddrCalls(mk, expected.GetAllAttributesAddrCalls, msg, args...) + return s.assertGetAllAttributesAddrCalls(mk, expected.GetAllAttributesAddr, msg, args...) } // NewGetAllAttributesAddrResult creates a new GetAllAttributesAddrResult from the provided stuff. @@ -258,9 +254,9 @@ type MockBankKeeper struct { // BankCalls contains all the calls that the mock bank keeper makes. type BankCalls struct { - SendCoinsCalls []*SendCoinsArgs - SendCoinsFromAccountToModuleCalls []*SendCoinsFromAccountToModuleArgs - InputOutputCoinsCalls []*InputOutputCoinsArgs + SendCoins []*SendCoinsArgs + SendCoinsFromAccountToModule []*SendCoinsFromAccountToModuleArgs + InputOutputCoins []*InputOutputCoinsArgs } // SendCoinsArgs is a record of a call that is made to SendCoins. @@ -318,7 +314,7 @@ func (k *MockBankKeeper) WithInputOutputCoinsResults(errs ...string) *MockBankKe } func (k *MockBankKeeper) SendCoins(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { - k.Calls.SendCoinsCalls = append(k.Calls.SendCoinsCalls, NewSendCoinsArgs(ctx, fromAddr, toAddr, amt)) + k.Calls.SendCoins = append(k.Calls.SendCoins, NewSendCoinsArgs(ctx, fromAddr, toAddr, amt)) var err error if len(k.SendCoinsResultsQueue) > 0 { if len(k.SendCoinsResultsQueue[0]) > 0 { @@ -330,7 +326,7 @@ func (k *MockBankKeeper) SendCoins(ctx sdk.Context, fromAddr, toAddr sdk.AccAddr } func (k *MockBankKeeper) SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error { - k.Calls.SendCoinsFromAccountToModuleCalls = append(k.Calls.SendCoinsFromAccountToModuleCalls, + k.Calls.SendCoinsFromAccountToModule = append(k.Calls.SendCoinsFromAccountToModule, NewSendCoinsFromAccountToModuleArgs(ctx, senderAddr, recipientModule, amt)) var err error if len(k.SendCoinsFromAccountToModuleResultsQueue) > 0 { @@ -343,7 +339,7 @@ func (k *MockBankKeeper) SendCoinsFromAccountToModule(ctx sdk.Context, senderAdd } func (k *MockBankKeeper) InputOutputCoins(ctx sdk.Context, inputs []banktypes.Input, outputs []banktypes.Output) error { - k.Calls.InputOutputCoinsCalls = append(k.Calls.InputOutputCoinsCalls, NewInputOutputCoinsArgs(ctx, inputs, outputs)) + k.Calls.InputOutputCoins = append(k.Calls.InputOutputCoins, NewInputOutputCoinsArgs(ctx, inputs, outputs)) var err error if len(k.InputOutputCoinsResultsQueue) > 0 { if len(k.InputOutputCoinsResultsQueue[0]) > 0 { @@ -354,34 +350,34 @@ func (k *MockBankKeeper) InputOutputCoins(ctx sdk.Context, inputs []banktypes.In return err } -// assertSendCoinsCalls asserts that a mock keeper's SendCoinsCalls match the provided expected calls. +// assertSendCoinsCalls asserts that a mock keeper's Calls.SendCoins match the provided expected calls. func (s *TestSuite) assertSendCoinsCalls(mk *MockBankKeeper, expected []*SendCoinsArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.SendCoinsCalls, s.sendCoinsArgsString, + return assertEqualSlice(s, expected, mk.Calls.SendCoins, s.sendCoinsArgsString, msg+" SendCoins calls", args...) } // assertSendCoinsFromAccountToModuleCalls asserts that a mock keeper's -// SendCoinsFromAccountToModuleCalls match the provided expected calls. +// Calls.SendCoinsFromAccountToModule match the provided expected calls. func (s *TestSuite) assertSendCoinsFromAccountToModuleCalls(mk *MockBankKeeper, expected []*SendCoinsFromAccountToModuleArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.SendCoinsFromAccountToModuleCalls, s.sendCoinsFromAccountToModuleArgsString, + return assertEqualSlice(s, expected, mk.Calls.SendCoinsFromAccountToModule, s.sendCoinsFromAccountToModuleArgsString, msg+" SendCoinsFromAccountToModule calls", args...) } -// assertInputOutputCoinsCalls asserts that a mock keeper's InputOutputCoinsCalls match the provided expected calls. +// assertInputOutputCoinsCalls asserts that a mock keeper's Calls.InputOutputCoins match the provided expected calls. func (s *TestSuite) assertInputOutputCoinsCalls(mk *MockBankKeeper, expected []*InputOutputCoinsArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.InputOutputCoinsCalls, s.inputOutputCoinsArgsString, + return assertEqualSlice(s, expected, mk.Calls.InputOutputCoins, s.inputOutputCoinsArgsString, msg+" InputOutputCoins calls", args...) } // assertBankKeeperCalls asserts that all the calls made to a mock bank keeper match the provided expected calls. func (s *TestSuite) assertBankKeeperCalls(mk *MockBankKeeper, expected BankCalls, msg string, args ...interface{}) bool { s.T().Helper() - rv := s.assertSendCoinsCalls(mk, expected.SendCoinsCalls, msg, args...) - rv = s.assertSendCoinsFromAccountToModuleCalls(mk, expected.SendCoinsFromAccountToModuleCalls, msg, args...) && rv - return s.assertInputOutputCoinsCalls(mk, expected.InputOutputCoinsCalls, msg, args...) && rv + rv := s.assertSendCoinsCalls(mk, expected.SendCoins, msg, args...) + rv = s.assertInputOutputCoinsCalls(mk, expected.InputOutputCoins, msg, args...) && rv + return s.assertSendCoinsFromAccountToModuleCalls(mk, expected.SendCoinsFromAccountToModule, msg, args...) && rv } // NewSendCoinsArgs creates a new record of args provided to a call to SendCoins. @@ -440,8 +436,7 @@ func (s *TestSuite) inputString(a banktypes.Input) string { // inputsString creates a string of a slice of banktypes.Input substituting the address names as possible. func (s *TestSuite) inputsString(vals []banktypes.Input) string { - strs := toStrings(vals, s.inputString) - return fmt.Sprintf("{%s}", strings.Join(strs, ", ")) + return fmt.Sprintf("{%s}", sliceString(vals, s.inputString)) } // outputString creates a string of a banktypes.Output substituting the address names as possible. @@ -451,8 +446,7 @@ func (s *TestSuite) outputString(a banktypes.Output) string { // outputsString creates a string of a slice of banktypes.Output substituting the address names as possible. func (s *TestSuite) outputsString(vals []banktypes.Output) string { - strs := toStrings(vals, s.outputString) - return fmt.Sprintf("{%s}", strings.Join(strs, ", ")) + return fmt.Sprintf("{%s}", sliceString(vals, s.outputString)) } // ############################################################################# @@ -473,9 +467,9 @@ type MockHoldKeeper struct { // HoldCalls contains all the calls that the mock hold keeper makes. type HoldCalls struct { - AddHoldCalls []*AddHoldArgs - ReleaseHoldCalls []*ReleaseHoldArgs - GetHoldCoinCalls []*GetHoldCoinArgs + AddHold []*AddHoldArgs + ReleaseHold []*ReleaseHoldArgs + GetHoldCoin []*GetHoldCoinArgs } // AddHoldArgs is a record of a call that is made to AddHold. @@ -560,7 +554,7 @@ func (k *MockHoldKeeper) WithGetHoldCoinErrorResult(addr sdk.AccAddress, denom s } func (k *MockHoldKeeper) AddHold(_ sdk.Context, addr sdk.AccAddress, funds sdk.Coins, reason string) error { - k.Calls.AddHoldCalls = append(k.Calls.AddHoldCalls, NewAddHoldArgs(addr, funds, reason)) + k.Calls.AddHold = append(k.Calls.AddHold, NewAddHoldArgs(addr, funds, reason)) var err error if len(k.AddHoldResultsQueue) > 0 { if len(k.AddHoldResultsQueue[0]) > 0 { @@ -572,7 +566,7 @@ func (k *MockHoldKeeper) AddHold(_ sdk.Context, addr sdk.AccAddress, funds sdk.C } func (k *MockHoldKeeper) ReleaseHold(_ sdk.Context, addr sdk.AccAddress, funds sdk.Coins) error { - k.Calls.ReleaseHoldCalls = append(k.Calls.ReleaseHoldCalls, NewReleaseHoldArgs(addr, funds)) + k.Calls.ReleaseHold = append(k.Calls.ReleaseHold, NewReleaseHoldArgs(addr, funds)) var err error if len(k.ReleaseHoldResultsQueue) > 0 { if len(k.ReleaseHoldResultsQueue[0]) > 0 { @@ -584,7 +578,7 @@ func (k *MockHoldKeeper) ReleaseHold(_ sdk.Context, addr sdk.AccAddress, funds s } func (k *MockHoldKeeper) GetHoldCoin(_ sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { - k.Calls.GetHoldCoinCalls = append(k.Calls.GetHoldCoinCalls, NewGetHoldCoinArgs(addr, denom)) + k.Calls.GetHoldCoin = append(k.Calls.GetHoldCoin, NewGetHoldCoinArgs(addr, denom)) if denomMap, aFound := k.GetHoldCoinResultsMap[string(addr)]; aFound { if rv, dFound := denomMap[denom]; dFound { return sdk.NewCoin(denom, rv.amount), rv.err @@ -593,33 +587,33 @@ func (k *MockHoldKeeper) GetHoldCoin(_ sdk.Context, addr sdk.AccAddress, denom s return sdk.NewInt64Coin(denom, 0), nil } -// assertAddHoldCalls asserts that a mock keeper's AddHoldCalls match the provided expected calls. +// assertAddHoldCalls asserts that a mock keeper's Calls.AddHold match the provided expected calls. func (s *TestSuite) assertAddHoldCalls(mk *MockHoldKeeper, expected []*AddHoldArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.AddHoldCalls, s.addHoldArgsString, - msg+" AddHoldCalls calls", args...) + return assertEqualSlice(s, expected, mk.Calls.AddHold, s.addHoldArgsString, + msg+" AddHold calls", args...) } -// assertReleaseHoldCalls asserts that a mock keeper's ReleaseHoldCalls match the provided expected calls. +// assertReleaseHoldCalls asserts that a mock keeper's Calls.ReleaseHold match the provided expected calls. func (s *TestSuite) assertReleaseHoldCalls(mk *MockHoldKeeper, expected []*ReleaseHoldArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.ReleaseHoldCalls, s.releaseHoldArgsString, - msg+" ReleaseHoldCalls calls", args...) + return assertEqualSlice(s, expected, mk.Calls.ReleaseHold, s.releaseHoldArgsString, + msg+" ReleaseHold calls", args...) } -// assertGetHoldCoinCalls asserts that a mock keeper's GetHoldCoinCalls match the provided expected calls. +// assertGetHoldCoinCalls asserts that a mock keeper's Calls.GetHoldCoin match the provided expected calls. func (s *TestSuite) assertGetHoldCoinCalls(mk *MockHoldKeeper, expected []*GetHoldCoinArgs, msg string, args ...interface{}) bool { s.T().Helper() - return assertEqualSlice(s, expected, mk.Calls.GetHoldCoinCalls, s.getHoldCoinArgsString, - msg+" GetHoldCoinCalls calls", args...) + return assertEqualSlice(s, expected, mk.Calls.GetHoldCoin, s.getHoldCoinArgsString, + msg+" GetHoldCoin calls", args...) } // assertHoldKeeperCalls asserts that all the calls made to a mock hold keeper match the provided expected calls. func (s *TestSuite) assertHoldKeeperCalls(mk *MockHoldKeeper, expected HoldCalls, msg string, args ...interface{}) bool { s.T().Helper() - rv := s.assertAddHoldCalls(mk, expected.AddHoldCalls, msg, args...) - rv = s.assertReleaseHoldCalls(mk, expected.ReleaseHoldCalls, msg, args...) && rv - return s.assertGetHoldCoinCalls(mk, expected.GetHoldCoinCalls, msg, args...) && rv + rv := s.assertAddHoldCalls(mk, expected.AddHold, msg, args...) + rv = s.assertReleaseHoldCalls(mk, expected.ReleaseHold, msg, args...) && rv + return s.assertGetHoldCoinCalls(mk, expected.GetHoldCoin, msg, args...) && rv } // NewAddHoldArgs creates a new record of args provided to a call to AddHold. diff --git a/x/exchange/keeper/msg_server.go b/x/exchange/keeper/msg_server.go index 16c5756736..962bf83236 100644 --- a/x/exchange/keeper/msg_server.go +++ b/x/exchange/keeper/msg_server.go @@ -94,7 +94,7 @@ func (k MsgServer) MarketSettle(goCtx context.Context, msg *exchange.MsgMarketSe func (k MsgServer) MarketSetOrderExternalID(goCtx context.Context, msg *exchange.MsgMarketSetOrderExternalIDRequest) (*exchange.MsgMarketSetOrderExternalIDResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) if !k.CanSetIDs(ctx, msg.MarketId, msg.Admin) { - return nil, permError("set uuids on orders for", msg.Admin, msg.MarketId) + return nil, permError("set external ids on orders for", msg.Admin, msg.MarketId) } err := k.SetOrderExternalID(ctx, msg.MarketId, msg.OrderId, msg.ExternalId) if err != nil { @@ -109,9 +109,8 @@ func (k MsgServer) MarketWithdraw(goCtx context.Context, msg *exchange.MsgMarket if !k.CanWithdrawMarketFunds(ctx, msg.MarketId, msg.Admin) { return nil, permError("withdraw from", msg.Admin, msg.MarketId) } - admin := sdk.MustAccAddressFromBech32(msg.Admin) toAddr := sdk.MustAccAddressFromBech32(msg.ToAddress) - err := k.WithdrawMarketFunds(ctx, msg.MarketId, toAddr, msg.Amount, admin) + err := k.WithdrawMarketFunds(ctx, msg.MarketId, toAddr, msg.Amount, msg.Admin) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } @@ -124,8 +123,7 @@ func (k MsgServer) MarketUpdateDetails(goCtx context.Context, msg *exchange.MsgM if !k.CanUpdateMarket(ctx, msg.MarketId, msg.Admin) { return nil, permError("update", msg.Admin, msg.MarketId) } - admin := sdk.MustAccAddressFromBech32(msg.Admin) - err := k.UpdateMarketDetails(ctx, msg.MarketId, msg.MarketDetails, admin) + err := k.UpdateMarketDetails(ctx, msg.MarketId, msg.MarketDetails, msg.Admin) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } @@ -138,8 +136,7 @@ func (k MsgServer) MarketUpdateEnabled(goCtx context.Context, msg *exchange.MsgM if !k.CanUpdateMarket(ctx, msg.MarketId, msg.Admin) { return nil, permError("update", msg.Admin, msg.MarketId) } - admin := sdk.MustAccAddressFromBech32(msg.Admin) - err := k.UpdateMarketActive(ctx, msg.MarketId, msg.AcceptingOrders, admin) + err := k.UpdateMarketActive(ctx, msg.MarketId, msg.AcceptingOrders, msg.Admin) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } @@ -152,8 +149,7 @@ func (k MsgServer) MarketUpdateUserSettle(goCtx context.Context, msg *exchange.M if !k.CanUpdateMarket(ctx, msg.MarketId, msg.Admin) { return nil, permError("update", msg.Admin, msg.MarketId) } - admin := sdk.MustAccAddressFromBech32(msg.Admin) - err := k.UpdateUserSettlementAllowed(ctx, msg.MarketId, msg.AllowUserSettlement, admin) + err := k.UpdateUserSettlementAllowed(ctx, msg.MarketId, msg.AllowUserSettlement, msg.Admin) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } diff --git a/x/exchange/keeper/msg_server_test.go b/x/exchange/keeper/msg_server_test.go index 73ed058097..677aa48bf6 100644 --- a/x/exchange/keeper/msg_server_test.go +++ b/x/exchange/keeper/msg_server_test.go @@ -1,35 +1,3272 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestNewMsgServer() +import ( + "context" + "fmt" -// TODO[1658]: func (s *TestSuite) TestMsgServer_CreateAsk() + abci "github.com/tendermint/tendermint/abci/types" -// TODO[1658]: func (s *TestSuite) TestMsgServer_CreateBid() + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/bank/testutil" -// TODO[1658]: func (s *TestSuite) TestMsgServer_CancelOrder() + "github.com/provenance-io/provenance/testutil/assertions" + attrtypes "github.com/provenance-io/provenance/x/attribute/types" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" + "github.com/provenance-io/provenance/x/hold" + markertypes "github.com/provenance-io/provenance/x/marker/types" +) -// TODO[1658]: func (s *TestSuite) TestMsgServer_FillBids() +// All of the msg_server endpoints are merely wrappers on other keeper functions, which +// are (hopefully) extensively tested. So, in here, it's some superficial testing, but +// without the mocks so that actual interaction with the other modules can be checked. -// TODO[1658]: func (s *TestSuite) TestMsgServer_FillAsks() +// invReqErr is the error added by sdkerrors.ErrInvalidRequest. +const invReqErr = "invalid request" -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketSettle() +// msgServerTestDef is the definition of a MsgServer endpoint to be tested. +// R is the request Msg type. S is the response message type. +// F is a type that holds arguments to provide to the followup function. +type msgServerTestDef[R any, S any, F any] struct { + // endpointName is the name of the endpoint being tested. + endpointName string + // endpoint is the endpoint function to invoke. + endpoint func(goCtx context.Context, msg *R) (*S, error) + // expResp is the expected response from the endpoint. It's only used if an error is not expected. + expResp *S + // followup is a function that runs any needed followup checks. + // This is only executed if an error neither expected, nor received. + // The TestSuite's ctx will be the cached context with the results of the setup and endpoint applied. + followup func(msg *R, fArgs F) +} -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketSetOrderExternalID() +// msgServerTestCase is a test case for a MsgServer endpoint +// R is the request Msg type. +// F is a type that holds arguments to provide to the followup function. +type msgServerTestCase[R any, F any] struct { + // name is the name of the test case. + name string + // setup is a function that does any needed app/state setup. + // A cached context is used for tests, so this setup will not carry over between test cases. + setup func() + // msg is the sdk.Msg to provide to the endpoint. + msg R + // expInErr is the strings that are expected to be in the error returned by the endpoint. + // If empty, that error is expected to be nil. + expInErr []string + // fArgs are any args to provide to the followup function. + fArgs F + // expEvents are the typed events that should be emitted. + // These are only checked if an error is neither expected, nor received. + expEvents sdk.Events +} -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketWithdraw() +// runMsgServerTestCase runs a unit test on a MsgServer endpoint. +// A cached context is used so each test case won't affect the others. +// R is the request Msg type. S is the response Msg type. +// F is a type that holds arguments to provide to the td.followup function. +func runMsgServerTestCase[R any, S any, F any](s *TestSuite, td msgServerTestDef[R, S, F], tc msgServerTestCase[R, F]) { + s.T().Helper() + origCtx := s.ctx + defer func() { + s.ctx = origCtx + }() + s.ctx, _ = s.ctx.CacheContext() -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketUpdateDetails() + var expResp *S + if len(tc.expInErr) == 0 { + expResp = td.expResp + } -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketUpdateEnabled() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketUpdateUserSettle() + em := sdk.NewEventManager() + s.ctx = s.ctx.WithEventManager(em) + goCtx := sdk.WrapSDKContext(s.ctx) + var resp *S + var err error + testFunc := func() { + resp, err = td.endpoint(goCtx, &tc.msg) + } + s.Require().NotPanicsf(testFunc, td.endpointName) + s.assertErrorContentsf(err, tc.expInErr, "%s error", td.endpointName) + s.Assert().Equalf(expResp, resp, "%s response", td.endpointName) -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketManagePermissions() + if len(tc.expInErr) > 0 || err != nil { + return + } -// TODO[1658]: func (s *TestSuite) TestMsgServer_MarketManageReqAttrs() + actEvents := em.Events() + s.assertEqualEvents(tc.expEvents, actEvents, "%s events", td.endpointName) -// TODO[1658]: func (s *TestSuite) TestMsgServer_GovCreateMarket() + td.followup(&tc.msg, tc.fArgs) +} -// TODO[1658]: func (s *TestSuite) TestMsgServer_GovManageFees() +// newAttr creates a new EventAttribute with the provided key and value. +func (s *TestSuite) newAttr(key, value string) abci.EventAttribute { + return abci.EventAttribute{Key: []byte(key), Value: []byte(value)} +} -// TODO[1658]: func (s *TestSuite) TestMsgServer_GovUpdateParams() +// eventCoinSpent creates a new "coin_spent" event (emitted by the bank module). +func (s *TestSuite) eventCoinSpent(spender sdk.AccAddress, amount string) sdk.Event { + return sdk.Event{ + Type: "coin_spent", + Attributes: []abci.EventAttribute{ + s.newAttr("spender", spender.String()), + s.newAttr("amount", amount), + }, + } +} + +// eventCoinReceived creates a new "coin_received" event (emitted by the bank module). +func (s *TestSuite) eventCoinReceived(receiver sdk.AccAddress, amount string) sdk.Event { + return sdk.Event{ + Type: "coin_received", + Attributes: []abci.EventAttribute{ + s.newAttr("receiver", receiver.String()), + s.newAttr("amount", amount), + }, + } +} + +// eventTransfer creates a new "transfer" event (emitted by the bank module). +func (s *TestSuite) eventTransfer(recipient, sender sdk.AccAddress, amount string) sdk.Event { + rv := sdk.Event{Type: "transfer"} + if len(recipient) > 0 { + rv.Attributes = append(rv.Attributes, s.newAttr("recipient", recipient.String())) + } + if len(sender) > 0 { + rv.Attributes = append(rv.Attributes, s.newAttr("sender", sender.String())) + } + rv.Attributes = append(rv.Attributes, s.newAttr("amount", amount)) + return rv +} + +// eventMessageSender creates a new "message" event with a "sender" attr (emitted by the bank module). +func (s *TestSuite) eventMessageSender(sender sdk.AccAddress) sdk.Event { + return sdk.Event{ + Type: "message", + Attributes: []abci.EventAttribute{s.newAttr("sender", sender.String())}, + } +} + +// eventHoldAdded creates a new event emitted when a hold is added (emitted by the hold module). +func (s *TestSuite) eventHoldAdded(addr sdk.AccAddress, amount string, orderID uint64) sdk.Event { + return s.untypeEvent(&hold.EventHoldAdded{ + Address: addr.String(), Amount: amount, Reason: fmt.Sprintf("x/exchange: order %d", orderID), + }) +} + +// eventHoldAdded creates a new event emitted when a hold is released (emitted by the hold module). +func (s *TestSuite) eventHoldReleased(addr sdk.AccAddress, amount string) sdk.Event { + return s.untypeEvent(&hold.EventHoldReleased{Address: addr.String(), Amount: amount}) +} + +// requireFundAccount calls testutil.FundAccount, making sure it doesn't panic or return an error. +func (s *TestSuite) requireFundAccount(addr sdk.AccAddress, coins string) { + assertions.RequireNotPanicsNoErrorf(s.T(), func() error { + return testutil.FundAccount(s.app.BankKeeper, s.ctx, addr, s.coins(coins)) + }, "FundAccount(%s, %q)", s.getAddrName(addr), coins) +} + +// requireAddHold calls s.app.HoldKeeper.AddHold, making sure it doesn't panic or return an error. +func (s *TestSuite) requireAddHold(addr sdk.AccAddress, holdCoins string, orderID uint64) { + coins := s.coins(holdCoins) + reason := fmt.Sprintf("test hold on order %d", orderID) + assertions.RequireNotPanicsNoErrorf(s.T(), func() error { + return s.app.HoldKeeper.AddHold(s.ctx, addr, coins, reason) + }, "AddHold(%s, %q, %q)", s.getAddrName(addr), holdCoins, reason) +} + +// requireSetNameRecord creates a name record, requiring it to not error. +func (s *TestSuite) requireSetNameRecord(name string, owner sdk.AccAddress) { + err := s.app.NameKeeper.SetNameRecord(s.ctx, name, owner, true) + s.Require().NoError(err, "NameKeeper.SetNameRecord(%q, %s, true)", name, s.getAddrName(owner)) +} + +// requireSetAttr creates an attribute with the given name on the given addr, requiring it to not error. +func (s *TestSuite) requireSetAttr(addr sdk.AccAddress, name string, owner sdk.AccAddress) { + attr := attrtypes.Attribute{ + Name: name, + Value: []byte("value of " + name), + AttributeType: attrtypes.AttributeType_String, + Address: addr.String(), + } + err := s.app.AttributeKeeper.SetAttribute(s.ctx, attr, owner) + s.Require().NoError(err, "SetAttribute(%s, %s)", name, s.getAddrName(owner)) +} + +// requireQuarantineOptIn opts an address into quarantine, requiring it to not error. +func (s *TestSuite) requireQuarantineOptIn(addr sdk.AccAddress) { + err := s.app.QuarantineKeeper.SetOptIn(s.ctx, addr) + s.Require().NoError(err, "QuarantineKeeper.SetOptIn(%s)", s.getAddrName(addr)) +} + +// requireSanctionAddress sanctions an address, requiring it to not error. +func (s *TestSuite) requireSanctionAddress(addr sdk.AccAddress) { + err := s.app.SanctionKeeper.SanctionAddresses(s.ctx, addr) + s.Require().NoError(err, "SanctionAddresses(%s)", s.getAddrName(addr)) +} + +// requireAddFinalizeAndActivateMarker creates a marker, requiring it to not error. +func (s *TestSuite) requireAddFinalizeAndActivateMarker(coin sdk.Coin, manager sdk.AccAddress, reqAttrs ...string) { + markerAddr, err := markertypes.MarkerAddress(coin.Denom) + s.Require().NoError(err, "MarkerAddress(%q)", coin.Denom) + marker := &markertypes.MarkerAccount{ + BaseAccount: &authtypes.BaseAccount{Address: markerAddr.String()}, + Manager: manager.String(), + AccessControl: []markertypes.AccessGrant{ + { + Address: manager.String(), + Permissions: markertypes.AccessList{ + markertypes.Access_Mint, markertypes.Access_Burn, + markertypes.Access_Deposit, markertypes.Access_Withdraw, markertypes.Access_Delete, + markertypes.Access_Admin, markertypes.Access_Transfer, + }, + }, + }, + Status: markertypes.StatusProposed, + Denom: coin.Denom, + Supply: coin.Amount, + MarkerType: markertypes.MarkerType_RestrictedCoin, + SupplyFixed: true, + AllowGovernanceControl: true, + AllowForcedTransfer: true, + RequiredAttributes: reqAttrs, + } + nav := markertypes.NewNetAssetValue(s.coin("5navcoin"), 1) + err = s.app.MarkerKeeper.SetNetAssetValue(s.ctx, marker, nav, "testing") + s.Require().NoError(err, "SetNetAssetValue(%d)", coin.Denom) + err = s.app.MarkerKeeper.AddFinalizeAndActivateMarker(s.ctx, marker) + s.Require().NoError(err, "AddFinalizeAndActivateMarker(%s)", coin.Denom) +} + +// expBalances is the definition of an account's expected balance, hold, and spendable. +// Only the denoms provided are checked in each type. +type expBalances struct { + addr sdk.AccAddress + expBal []sdk.Coin + expHold []sdk.Coin + expSpend []sdk.Coin +} + +// checkBalances looks up the actual balances and asserts that they're the same as provided. +func (s *TestSuite) checkBalances(eb expBalances) bool { + addrName := s.getAddrName(eb.addr) + rv := true + + for _, expBal := range eb.expBal { + actBal := s.app.BankKeeper.GetBalance(s.ctx, eb.addr, expBal.Denom) + rv = s.Assert().Equalf(expBal.String(), actBal.String(), "actual balance of %s for %s", expBal.Denom, addrName) && rv + } + + for _, expHold := range eb.expHold { + actHold, err := s.app.HoldKeeper.GetHoldCoin(s.ctx, eb.addr, expHold.Denom) + if s.Assert().NoError(err, "GetHoldCoin(%s, %q)", addrName, expHold.Denom) { + rv = s.Assert().Equalf(expHold.String(), actHold.String(), "amount on hold of %s for %s", expHold.Denom, addrName) && rv + } else { + rv = false + } + } + + actSpendBal := s.app.BankKeeper.SpendableCoins(s.ctx, eb.addr) + for _, expSpend := range eb.expSpend { + actSpend := sdk.Coin{Denom: expSpend.Denom, Amount: actSpendBal.AmountOf(expSpend.Denom)} + rv = s.Assert().Equalf(expSpend.String(), actSpend.String(), "spendable balance of %s for %s", expSpend.Denom, addrName) && rv + } + + return rv +} + +// zeroCoin creates a coin in the given denom with a zero amount. +// Handy for putting in an expBalances to check that a denom is zero. +func (s *TestSuite) zeroCoin(denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.ZeroInt()} +} + +// zeroCoins creates a coin for each denom, each with a zero amount. +// Handy for putting in an expBalances to check that several denoms are zero. +func (s *TestSuite) zeroCoins(denoms ...string) []sdk.Coin { + rv := make([]sdk.Coin, len(denoms)) + for i, denom := range denoms { + rv[i] = s.zeroCoin(denom) + } + return rv +} + +func (s *TestSuite) TestMsgServer_CreateAsk() { + type followupArgs struct { + expOrderID uint64 + expBal expBalances + } + testDef := msgServerTestDef[exchange.MsgCreateAskRequest, exchange.MsgCreateAskResponse, followupArgs]{ + endpointName: "CreateAsk", + endpoint: keeper.NewMsgServer(s.k).CreateAsk, + followup: func(_ *exchange.MsgCreateAskRequest, fargs followupArgs) { + s.checkBalances(fargs.expBal) + }, + } + + tests := []msgServerTestCase[exchange.MsgCreateAskRequest, followupArgs]{ + { + name: "invalid msg", + setup: nil, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 0, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + }, + expInErr: []string{invReqErr, "invalid market id: must not be zero"}, + }, + { + name: "market does not exist", + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 7, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + }, + expInErr: []string{invReqErr, "market 7 does not exist"}, + }, + { + name: "cannot collect creation fee", + setup: func() { + s.requireFundAccount(s.addr1, "9fig") + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("10fig"), + }) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + OrderCreationFee: s.coinP("10fig"), + }, + expInErr: []string{ + invReqErr, "error collecting ask order creation fee", + "error transferring 10fig from " + s.addr1.String() + " to market 1", + "spendable balance 9fig is smaller than 10fig", + "insufficient funds", + }, + }, + { + name: "duplicate external id", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1, AcceptingOrders: true}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr5.String(), + Assets: s.coin("1apple"), + Price: s.coin("1peach"), + ExternalId: "dupeid", + })) + keeper.SetLastOrderID(store, 10) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1peach"), + ExternalId: "dupeid", + }, + }, + expInErr: []string{ + invReqErr, "error storing ask order", + "external id \"dupeid\" is already in use by order 8: cannot be used for order 11", + }, + }, + { + name: "assets not in account", + setup: func() { + s.requireFundAccount(s.addr1, "9apple") + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 3, AcceptingOrders: true}) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("10peach"), + }, + }, + expInErr: []string{ + invReqErr, "error placing hold for ask order 1", + "account " + s.addr1.String() + " spendable balance 9apple is less than hold amount 10apple", + }, + }, + { + name: "settlement fee not in account", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("10fig"), + FeeSellerSettlementFlat: s.coins("5fig"), + }) + s.requireFundAccount(s.addr1, "100apple,20fig") + s.requireAddHold(s.addr1, "6fig", 0) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), + Assets: s.coin("100apple"), Price: s.coin("10peach"), + SellerSettlementFlatFee: s.coinP("5fig"), + }, + OrderCreationFee: s.coinP("10fig"), + }, + expInErr: []string{ + invReqErr, "error placing hold for ask order 1", + "account " + s.addr1.String() + " spendable balance 4fig is less than hold amount 5fig", + }, + }, + { + name: "okay: no settlement fee", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 5, AcceptingOrders: true}) + s.requireFundAccount(s.addr2, "100apple,100fig,100pear") + keeper.SetLastOrderID(s.getStore(), 83) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 5, Seller: s.addr2.String(), + Assets: s.coin("60apple"), Price: s.coin("45pear"), + }, + }, + fArgs: followupArgs{ + expOrderID: 84, + expBal: expBalances{ + addr: s.addr2, + expBal: s.coins("100apple,100fig,100pear"), + expHold: []sdk.Coin{s.coin("60apple"), s.zeroCoin("fig"), s.zeroCoin("pear")}, + expSpend: s.coins("40apple,100fig,100pear"), + }, + }, + expEvents: sdk.Events{ + s.eventHoldAdded(s.addr2, "60apple", 84), + s.untypeEvent(&exchange.EventOrderCreated{ + OrderId: 84, OrderType: "ask", MarketId: 5, ExternalId: "", + }), + }, + }, + { + name: "okay: settlement fee same denom as price", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("8pear"), + FeeSellerSettlementFlat: s.coins("12pear"), + }) + s.requireFundAccount(s.addr2, "100apple,100fig,100pear") + keeper.SetLastOrderID(s.getStore(), 6) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 2, Seller: s.addr2.String(), + Assets: s.coin("75apple"), Price: s.coin("45pear"), + SellerSettlementFlatFee: s.coinP("12pear"), + ExternalId: "just-an-id", + }, + OrderCreationFee: s.coinP("8pear"), + }, + fArgs: followupArgs{ + expOrderID: 7, + expBal: expBalances{ + addr: s.addr2, + expBal: s.coins("100apple,100fig,92pear"), + expHold: []sdk.Coin{s.coin("75apple"), s.zeroCoin("fig"), s.zeroCoin("pear")}, + expSpend: s.coins("25apple,100fig,92pear"), + }, + }, + expEvents: sdk.Events{ + s.eventCoinSpent(s.addr2, "8pear"), + s.eventCoinReceived(s.marketAddr2, "8pear"), + s.eventTransfer(s.marketAddr2, s.addr2, "8pear"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.marketAddr2, "1pear"), + s.eventCoinReceived(s.feeCollectorAddr, "1pear"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr2, "1pear"), + s.eventMessageSender(s.marketAddr2), + s.eventHoldAdded(s.addr2, "75apple", 7), + s.untypeEvent(&exchange.EventOrderCreated{ + OrderId: 7, OrderType: "ask", MarketId: 2, ExternalId: "just-an-id", + }), + }, + }, + { + name: "okay: settlement fee diff denom from price", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("8fig"), + FeeSellerSettlementFlat: s.coins("12fig"), + }) + s.requireFundAccount(s.addr2, "100apple,100fig,100pear") + keeper.SetLastOrderID(s.getStore(), 12344) + }, + msg: exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 3, Seller: s.addr2.String(), + Assets: s.coin("75apple"), Price: s.coin("45pear"), + SellerSettlementFlatFee: s.coinP("12fig"), + }, + OrderCreationFee: s.coinP("8fig"), + }, + fArgs: followupArgs{ + expOrderID: 12345, + expBal: expBalances{ + addr: s.addr2, + expBal: s.coins("100apple,92fig,100pear"), + expHold: []sdk.Coin{s.coin("75apple"), s.coin("12fig"), s.zeroCoin("pear")}, + expSpend: s.coins("25apple,80fig,100pear"), + }, + }, + expEvents: sdk.Events{ + s.eventCoinSpent(s.addr2, "8fig"), + s.eventCoinReceived(s.marketAddr3, "8fig"), + s.eventTransfer(s.marketAddr3, s.addr2, "8fig"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.marketAddr3, "1fig"), + s.eventCoinReceived(s.feeCollectorAddr, "1fig"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr3, "1fig"), + s.eventMessageSender(s.marketAddr3), + s.eventHoldAdded(s.addr2, "75apple,12fig", 12345), + s.untypeEvent(&exchange.EventOrderCreated{ + OrderId: 12345, OrderType: "ask", MarketId: 3, ExternalId: "", + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + td := testDef + td.expResp = &exchange.MsgCreateAskResponse{OrderId: tc.fArgs.expOrderID} + runMsgServerTestCase(s, td, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_CreateBid() { + type followupArgs struct { + expOrderID uint64 + expBal expBalances + } + testDef := msgServerTestDef[exchange.MsgCreateBidRequest, exchange.MsgCreateBidResponse, followupArgs]{ + endpointName: "CreateBid", + endpoint: keeper.NewMsgServer(s.k).CreateBid, + followup: func(_ *exchange.MsgCreateBidRequest, fargs followupArgs) { + s.checkBalances(fargs.expBal) + }, + } + + tests := []msgServerTestCase[exchange.MsgCreateBidRequest, followupArgs]{ + { + name: "invalid msg", + setup: nil, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 0, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + }, + expInErr: []string{invReqErr, "invalid market id: must not be zero"}, + }, + { + name: "market does not exist", + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 7, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + }, + expInErr: []string{invReqErr, "market 7 does not exist"}, + }, + { + name: "cannot collect creation fee", + setup: func() { + s.requireFundAccount(s.addr1, "9fig") + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("10fig"), + }) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1peach"), + }, + OrderCreationFee: s.coinP("10fig"), + }, + expInErr: []string{ + invReqErr, "error collecting bid order creation fee", + "error transferring 10fig from " + s.addr1.String() + " to market 1", + "spendable balance 9fig is smaller than 10fig", + "insufficient funds", + }, + }, + { + name: "duplicate external id", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 1, AcceptingOrders: true}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr5.String(), + Assets: s.coin("1apple"), + Price: s.coin("1peach"), + ExternalId: "dupeid", + })) + keeper.SetLastOrderID(store, 10) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1peach"), + ExternalId: "dupeid", + }, + }, + expInErr: []string{ + invReqErr, "error storing bid order", + "external id \"dupeid\" is already in use by order 8: cannot be used for order 11", + }, + }, + { + name: "price not in account", + setup: func() { + s.requireFundAccount(s.addr1, "9peach") + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 3, AcceptingOrders: true}) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 3, Buyer: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("10peach"), + }, + }, + expInErr: []string{ + invReqErr, "error placing hold for bid order 1", + "account " + s.addr1.String() + " spendable balance 9peach is less than hold amount 10peach", + }, + }, + { + name: "settlement fee not in account", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("10fig"), + FeeSellerSettlementFlat: s.coins("5fig"), + }) + s.requireFundAccount(s.addr1, "100peach,20fig") + s.requireAddHold(s.addr1, "6fig", 0) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 3, Buyer: s.addr1.String(), + Assets: s.coin("10apple"), Price: s.coin("100peach"), + BuyerSettlementFees: s.coins("5fig"), + }, + OrderCreationFee: s.coinP("10fig"), + }, + expInErr: []string{ + invReqErr, "error placing hold for bid order 1", + "account " + s.addr1.String() + " spendable balance 4fig is less than hold amount 5fig", + }, + }, + { + name: "okay: no settlement fee", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 2, AcceptingOrders: true}) + s.requireFundAccount(s.addr2, "100apple,100fig,100pear") + keeper.SetLastOrderID(s.getStore(), 83) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), + Assets: s.coin("60apple"), Price: s.coin("45pear"), + }, + }, + fArgs: followupArgs{ + expOrderID: 84, + expBal: expBalances{ + addr: s.addr2, + expBal: s.coins("100apple,100fig,100pear"), + expHold: []sdk.Coin{s.zeroCoin("apple"), s.zeroCoin("fig"), s.coin("45pear")}, + expSpend: s.coins("100apple,100fig,55pear"), + }, + }, + expEvents: sdk.Events{ + s.eventHoldAdded(s.addr2, "45pear", 84), + s.untypeEvent(&exchange.EventOrderCreated{ + OrderId: 84, OrderType: "bid", MarketId: 2, ExternalId: "", + }), + }, + }, + { + name: "okay: with settlement fee", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, + FeeCreateAskFlat: s.coins("8pear"), + FeeSellerSettlementFlat: s.coins("12pear"), + }) + s.requireFundAccount(s.addr2, "100apple,100fig,100pear") + keeper.SetLastOrderID(s.getStore(), 6) + }, + msg: exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), + Assets: s.coin("60apple"), Price: s.coin("75pear"), + BuyerSettlementFees: s.coins("12pear"), + ExternalId: "some-random-id", + }, + OrderCreationFee: s.coinP("8pear"), + }, + fArgs: followupArgs{ + expOrderID: 7, + expBal: expBalances{ + addr: s.addr2, + expBal: s.coins("100apple,100fig,92pear"), + expHold: []sdk.Coin{s.zeroCoin("apple"), s.zeroCoin("fig"), s.coin("87pear")}, + expSpend: s.coins("100apple,100fig,5pear"), + }, + }, + expEvents: sdk.Events{ + s.eventCoinSpent(s.addr2, "8pear"), + s.eventCoinReceived(s.marketAddr2, "8pear"), + s.eventTransfer(s.marketAddr2, s.addr2, "8pear"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.marketAddr2, "1pear"), + s.eventCoinReceived(s.feeCollectorAddr, "1pear"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr2, "1pear"), + s.eventMessageSender(s.marketAddr2), + s.eventHoldAdded(s.addr2, "87pear", 7), + s.untypeEvent(&exchange.EventOrderCreated{ + OrderId: 7, OrderType: "bid", MarketId: 2, ExternalId: "some-random-id", + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + td := testDef + td.expResp = &exchange.MsgCreateBidResponse{OrderId: tc.fArgs.expOrderID} + runMsgServerTestCase(s, td, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_CancelOrder() { + testDef := msgServerTestDef[exchange.MsgCancelOrderRequest, exchange.MsgCancelOrderResponse, expBalances]{ + endpointName: "CancelOrder", + endpoint: keeper.NewMsgServer(s.k).CancelOrder, + expResp: &exchange.MsgCancelOrderResponse{}, + followup: func(msg *exchange.MsgCancelOrderRequest, eb expBalances) { + order, err := s.k.GetOrder(s.ctx, msg.OrderId) + s.Assert().NoError(err, "GetOrder(%d) error", msg.OrderId) + s.Assert().Nil(order, "GetOrder(%d) order", msg.OrderId) + s.checkBalances(eb) + }, + } + + tests := []msgServerTestCase[exchange.MsgCancelOrderRequest, expBalances]{ + { + name: "order 0", + setup: nil, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr1.String(), OrderId: 0}, + expInErr: []string{invReqErr, "order 0 does not exist"}, + }, + { + name: "order does not exist", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 3}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithBid(&exchange.BidOrder{ + MarketId: 3, Buyer: s.addr3.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + })) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr2.String(), OrderId: 6}, + expInErr: []string{invReqErr, "order 6 does not exist"}, + }, + { + name: "wrong signer", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{MarketId: 2}) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(83).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + })) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr2.String(), OrderId: 83}, + expInErr: []string{invReqErr, "account " + s.addr2.String() + " does not have permission to cancel order 83"}, + }, + { + name: "market signer: ask", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_cancel)}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(44).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + })) + s.requireFundAccount(s.addr1, "10apple") + s.requireAddHold(s.addr1, "2apple", 44) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr5.String(), OrderId: 44}, + fArgs: expBalances{ + addr: s.addr1, + expBal: s.coins("10apple"), + expHold: s.coins("1apple"), + expSpend: s.coins("9apple"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr1, "1apple"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 44, CancelledBy: s.addr5.String(), MarketId: 2, ExternalId: "", + }), + }, + }, + { + name: "market signer: bid", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_cancel)}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(44).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + })) + s.requireFundAccount(s.addr1, "10pear") + s.requireAddHold(s.addr1, "1pear", 44) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr5.String(), OrderId: 44}, + fArgs: expBalances{ + addr: s.addr1, + expBal: s.coins("10pear"), + expHold: []sdk.Coin{s.zeroCoin("pear")}, + expSpend: s.coins("10pear"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr1, "1pear"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 44, CancelledBy: s.addr5.String(), MarketId: 2, ExternalId: "", + }), + }, + }, + { + name: "ask with diff fee denom from price", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(5555).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("10apple"), + Price: s.coin("5pear"), + SellerSettlementFlatFee: s.coinP("1fig"), + ExternalId: "ext-id-5555", + })) + s.requireFundAccount(s.addr1, "15apple,5fig") + s.requireAddHold(s.addr1, "10apple,1fig", 5555) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr1.String(), OrderId: 5555}, + fArgs: expBalances{ + addr: s.addr1, + expBal: s.coins("15apple,5fig"), + expHold: s.zeroCoins("apple", "fig"), + expSpend: s.coins("15apple,5fig"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr1, "10apple,1fig"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 5555, CancelledBy: s.addr1.String(), MarketId: 1, ExternalId: "ext-id-5555", + }), + }, + }, + { + name: "ask with same fee denom as price", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(98765).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: s.addr2.String(), + Assets: s.coin("10apple"), + Price: s.coin("5pear"), + SellerSettlementFlatFee: s.coinP("1pear"), + ExternalId: "whatever", + })) + s.requireFundAccount(s.addr2, "15apple,5fig") + s.requireAddHold(s.addr2, "10apple,1fig", 98765) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr2.String(), OrderId: 98765}, + fArgs: expBalances{ + addr: s.addr2, + expBal: s.coins("15apple,5fig"), + expHold: []sdk.Coin{s.zeroCoin("apple"), s.coin("1fig")}, + expSpend: s.coins("15apple,4fig"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr2, "10apple"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 98765, CancelledBy: s.addr2.String(), MarketId: 3, ExternalId: "whatever", + }), + }, + }, + { + name: "bid with diff fee denom from price", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(5555).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("10apple"), + Price: s.coin("5pear"), + BuyerSettlementFees: s.coins("1fig"), + ExternalId: "ext-id-5555", + })) + s.requireFundAccount(s.addr1, "15pear,5fig") + s.requireAddHold(s.addr1, "5pear,1fig", 5555) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr1.String(), OrderId: 5555}, + fArgs: expBalances{ + addr: s.addr1, + expBal: s.coins("15pear,5fig"), + expHold: s.zeroCoins("pear", "fig"), + expSpend: s.coins("15pear,5fig"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr1, "1fig,5pear"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 5555, CancelledBy: s.addr1.String(), MarketId: 1, ExternalId: "ext-id-5555", + }), + }, + }, + { + name: "bid with same fee denom as price", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(98765).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr2.String(), + Assets: s.coin("10apple"), + Price: s.coin("5pear"), + BuyerSettlementFees: s.coins("1pear"), + ExternalId: "whatever", + })) + s.requireFundAccount(s.addr2, "15pear,5fig") + s.requireAddHold(s.addr2, "6pear", 98765) + }, + msg: exchange.MsgCancelOrderRequest{Signer: s.addr2.String(), OrderId: 98765}, + fArgs: expBalances{ + addr: s.addr2, + expBal: s.coins("15pear,5fig"), + expHold: s.zeroCoins("pear", "fig"), + expSpend: s.coins("15pear,5fig"), + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr2, "6pear"), + s.untypeEvent(&exchange.EventOrderCancelled{ + OrderId: 98765, CancelledBy: s.addr2.String(), MarketId: 3, ExternalId: "whatever", + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_FillBids() { + testDef := msgServerTestDef[exchange.MsgFillBidsRequest, exchange.MsgFillBidsResponse, []expBalances]{ + endpointName: "FillBids", + endpoint: keeper.NewMsgServer(s.k).FillBids, + expResp: &exchange.MsgFillBidsResponse{}, + followup: func(msg *exchange.MsgFillBidsRequest, ebs []expBalances) { + for _, orderID := range msg.BidOrderIds { + order, err := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(err, "GetOrder(%d) error", orderID) + s.Assert().Nil(order, "GetOrder(%d) order", orderID) + } + + for _, eb := range ebs { + s.checkBalances(eb) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgFillBidsRequest, []expBalances]{ + { + name: "user can't create ask", + setup: func() { + s.requireSetNameRecord("almost.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "almost.gonna.have.it", s.addr5) + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + ReqAttrCreateAsk: []string{"not.gonna.have.it"}, + }) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("1apple"), + BidOrderIds: []uint64{1}, + }, + expInErr: []string{invReqErr, "account " + s.addr1.String() + " is not allowed to create ask orders in market 1"}, + }, + { + name: "one bid, both quarantined", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr2, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(54).WithBid(&exchange.BidOrder{ + MarketId: 3, Buyer: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr1, "50pear", 54) + + s.requireQuarantineOptIn(s.addr1) + s.requireQuarantineOptIn(s.addr2) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr2.String(), + MarketId: 3, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{54}, + }, + fArgs: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear")}, + expHold: s.zeroCoins("apple", "pear"), + expSpend: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear")}, + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("50pear")}, + expHold: s.zeroCoins("apple", "pear"), + expSpend: []sdk.Coin{s.zeroCoin("apple"), s.coin("50pear")}, + }, + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr1, "50pear"), + s.eventCoinSpent(s.addr2, "10apple"), + s.eventCoinReceived(s.addr1, "10apple"), + s.eventTransfer(s.addr1, s.addr2, "10apple"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr1, "50pear"), + s.eventCoinReceived(s.addr2, "50pear"), + s.eventTransfer(s.addr2, s.addr1, "50pear"), + s.eventMessageSender(s.addr1), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, + }), + }, + }, + { + name: "one bid, buyer sanctioned", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr4, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(77).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr1, "50pear", 77) + + s.requireSanctionAddress(s.addr1) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{77}, + }, + expInErr: []string{invReqErr, "cannot send from " + s.addr1.String(), "account is sanctioned"}, + }, + { + name: "one bid, seller sanctioned", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr4, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(77).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + + s.requireAddHold(s.addr1, "50pear", 77) + + s.requireSanctionAddress(s.addr4) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr4.String(), + MarketId: 2, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{77}, + }, + expInErr: []string{invReqErr, "cannot send from " + s.addr4.String(), "account is sanctioned"}, + }, + { + name: "one bid, buyer does not have asset marker's req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("10apple"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear") + s.requireFundAccount(s.addr1, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr2, "50pear", 4) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 2, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{4}, + }, + expInErr: []string{invReqErr, + "address " + s.addr2.String() + " does not contain the \"apple\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "one bid, seller does not have price marker's req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("50pear"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear") + s.requireFundAccount(s.addr1, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr2, "50pear", 4) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 2, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{4}, + }, + expInErr: []string{invReqErr, + "address " + s.addr1.String() + " does not contain the \"pear\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "market does not have req attr for fee denom", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("200fig"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear,100fig") + s.requireFundAccount(s.addr1, "10apple,100fig") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(12345).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + BuyerSettlementFees: s.coins("100fig"), + })) + s.requireAddHold(s.addr2, "50pear,100fig", 12345) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("10apple"), + BidOrderIds: []uint64{12345}, + SellerSettlementFlatFee: s.coinP("100fig"), + }, + expInErr: []string{invReqErr, + "address " + s.marketAddr1.String() + " does not contain the \"fig\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "okay: two bids, all req attrs and fees", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("13apple"), s.addr5, "*.gonna.have.it") + s.requireAddFinalizeAndActivateMarker(s.coin("70pear"), s.addr5, "*.gonna.have.it") + s.requireAddFinalizeAndActivateMarker(s.coin("300fig"), s.addr5, "*.gonna.have.it") + s.requireSetNameRecord("buyer.gonna.have.it", s.addr5) + s.requireSetNameRecord("seller.gonna.have.it", s.addr5) + s.requireSetNameRecord("market.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "seller.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "buyer.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr3, "buyer.gonna.have.it", s.addr5) + s.requireSetAttr(s.marketAddr1, "market.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr1, "13apple,100fig") + s.requireFundAccount(s.addr2, "50pear,100fig") + s.requireFundAccount(s.addr3, "20pear,100fig") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeCreateAskFlat: s.coins("10fig"), + FeeCreateBidFlat: s.coins("200fig"), + FeeSellerSettlementFlat: s.coins("5pear"), + FeeSellerSettlementRatios: s.ratios("35pear:2pear"), + FeeBuyerSettlementFlat: s.coins("30fig"), + FeeBuyerSettlementRatios: s.ratios("10pear:1fig"), + ReqAttrCreateAsk: []string{"*.gonna.have.it"}, + ReqAttrCreateBid: []string{"not.gonna.have.it"}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(12345).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + BuyerSettlementFees: s.coins("35fig"), ExternalId: "first order", + })) + s.requireAddHold(s.addr2, "50pear,35fig", 12345) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(98765).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("20pear"), + BuyerSettlementFees: s.coins("32fig"), ExternalId: "second order", + })) + s.requireAddHold(s.addr3, "20pear,32fig", 98765) + }, + msg: exchange.MsgFillBidsRequest{ + Seller: s.addr1.String(), + MarketId: 1, + TotalAssets: s.coins("13apple"), + BidOrderIds: []uint64{12345, 98765}, + SellerSettlementFlatFee: s.coinP("5pear"), + AskOrderCreationFee: s.coinP("10fig"), + }, + fArgs: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("61pear"), s.coin("90fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear"), s.coin("65fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr3, + expBal: []sdk.Coin{s.coin("3apple"), s.zeroCoin("pear"), s.coin("68fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.marketAddr1, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("8pear"), s.coin("72fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.feeCollectorAddr, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("1pear"), s.coin("5fig")}, + }, + }, + expEvents: sdk.Events{ + // Hold release events. + s.eventHoldReleased(s.addr2, "35fig,50pear"), + s.eventHoldReleased(s.addr3, "32fig,20pear"), + + // Asset transfer events. + s.eventCoinSpent(s.addr1, "13apple"), + s.eventMessageSender(s.addr1), + s.eventCoinReceived(s.addr2, "10apple"), + s.eventTransfer(s.addr2, nil, "10apple"), + s.eventCoinReceived(s.addr3, "3apple"), + s.eventTransfer(s.addr3, nil, "3apple"), + + // Price transfer events. + s.eventCoinSpent(s.addr2, "50pear"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr3, "20pear"), + s.eventMessageSender(s.addr3), + s.eventCoinReceived(s.addr1, "70pear"), + s.eventTransfer(s.addr1, nil, "70pear"), + + // Settlement fee transfer events. + s.eventCoinSpent(s.addr2, "35fig"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr3, "32fig"), + s.eventMessageSender(s.addr3), + s.eventCoinSpent(s.addr1, "9pear"), + s.eventMessageSender(s.addr1), + s.eventCoinReceived(s.marketAddr1, "67fig,9pear"), + s.eventTransfer(s.marketAddr1, nil, "67fig,9pear"), + + // Transfer of exchange portion of settlement fee. + s.eventCoinSpent(s.marketAddr1, "4fig,1pear"), + s.eventCoinReceived(s.feeCollectorAddr, "4fig,1pear"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr1, "4fig,1pear"), + s.eventMessageSender(s.marketAddr1), + + // Order filled events. + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 12345, + Assets: "10apple", + Price: "50pear", + Fees: "35fig", + MarketId: 1, + ExternalId: "first order", + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 98765, + Assets: "3apple", + Price: "20pear", + Fees: "32fig", + MarketId: 1, + ExternalId: "second order", + }), + + // Order creation fee events. + s.eventCoinSpent(s.addr1, "10fig"), + s.eventCoinReceived(s.marketAddr1, "10fig"), + s.eventTransfer(s.marketAddr1, s.addr1, "10fig"), + s.eventMessageSender(s.addr1), + + // Transfer of exchange portion of order creation fees. + s.eventCoinSpent(s.marketAddr1, "1fig"), + s.eventCoinReceived(s.feeCollectorAddr, "1fig"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr1, "1fig"), + s.eventMessageSender(s.marketAddr1), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_FillAsks() { + testDef := msgServerTestDef[exchange.MsgFillAsksRequest, exchange.MsgFillAsksResponse, []expBalances]{ + endpointName: "FillAsks", + endpoint: keeper.NewMsgServer(s.k).FillAsks, + expResp: &exchange.MsgFillAsksResponse{}, + followup: func(msg *exchange.MsgFillAsksRequest, ebs []expBalances) { + for _, orderID := range msg.AskOrderIds { + order, err := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(err, "GetOrder(%d) error", orderID) + s.Assert().Nil(order, "GetOrder(%d) order", orderID) + } + + for _, eb := range ebs { + s.checkBalances(eb) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgFillAsksRequest, []expBalances]{ + { + name: "user can't create bid", + setup: func() { + s.requireSetNameRecord("almost.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "almost.gonna.have.it", s.addr5) + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + ReqAttrCreateBid: []string{"not.gonna.have.it"}, + }) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("1pear"), + AskOrderIds: []uint64{1}, + }, + expInErr: []string{invReqErr, "account " + s.addr1.String() + " is not allowed to create bid orders in market 1"}, + }, + { + name: "one ask, both quarantined", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr2, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(54).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr2, "10apple", 54) + + s.requireQuarantineOptIn(s.addr1) + s.requireQuarantineOptIn(s.addr2) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 3, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{54}, + }, + fArgs: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear")}, + expHold: s.zeroCoins("apple", "pear"), + expSpend: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear")}, + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("50pear")}, + expHold: s.zeroCoins("apple", "pear"), + expSpend: []sdk.Coin{s.zeroCoin("apple"), s.coin("50pear")}, + }, + }, + expEvents: sdk.Events{ + s.eventHoldReleased(s.addr2, "10apple"), + s.eventCoinSpent(s.addr2, "10apple"), + s.eventCoinReceived(s.addr1, "10apple"), + s.eventTransfer(s.addr1, s.addr2, "10apple"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr1, "50pear"), + s.eventCoinReceived(s.addr2, "50pear"), + s.eventTransfer(s.addr2, s.addr1, "50pear"), + s.eventMessageSender(s.addr1), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, + }), + }, + }, + { + name: "one ask, buyer sanctioned", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr4, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(77).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr4.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr4, "10apple", 77) + + s.requireSanctionAddress(s.addr1) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 2, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{77}, + }, + expInErr: []string{invReqErr, "cannot send from " + s.addr1.String(), "account is sanctioned"}, + }, + { + name: "one ask, seller sanctioned", + setup: func() { + s.requireFundAccount(s.addr1, "50pear") + s.requireFundAccount(s.addr4, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(77).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr4.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr4, "10apple", 77) + + s.requireSanctionAddress(s.addr4) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 2, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{77}, + }, + expInErr: []string{invReqErr, "cannot send from " + s.addr4.String(), "account is sanctioned"}, + }, + { + name: "one ask, buyer does not have asset marker's req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("10apple"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear") + s.requireFundAccount(s.addr1, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr1, "10apple", 4) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr2.String(), + MarketId: 2, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{4}, + }, + expInErr: []string{invReqErr, + "address " + s.addr2.String() + " does not contain the \"apple\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "one ask, seller does not have price marker's req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("50pear"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear") + s.requireFundAccount(s.addr1, "10apple") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + })) + s.requireAddHold(s.addr1, "10apple", 4) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr2.String(), + MarketId: 2, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{4}, + }, + expInErr: []string{invReqErr, + "address " + s.addr1.String() + " does not contain the \"pear\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "market does not have req attr for fee denom", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("200fig"), s.addr5, "not.gonna.have.it") + s.requireSetNameRecord("not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "not.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "not.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr2, "50pear,100fig") + s.requireFundAccount(s.addr1, "10apple,100fig") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(12345).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + SellerSettlementFlatFee: s.coinP("100fig"), + })) + s.requireAddHold(s.addr1, "10apple,100fig", 12345) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr2.String(), + MarketId: 1, + TotalPrice: s.coin("50pear"), + AskOrderIds: []uint64{12345}, + BuyerSettlementFees: s.coins("100fig"), + }, + expInErr: []string{invReqErr, + "address " + s.marketAddr1.String() + " does not contain the \"fig\" required attribute: \"not.gonna.have.it\"", + }, + }, + { + name: "okay: two asks, all req attrs and fees", + setup: func() { + s.requireSetNameRecord("buyer.gonna.have.it", s.addr5) + s.requireSetNameRecord("seller.gonna.have.it", s.addr5) + s.requireSetNameRecord("market.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr1, "buyer.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr2, "seller.gonna.have.it", s.addr5) + s.requireSetAttr(s.addr3, "seller.gonna.have.it", s.addr5) + s.requireSetAttr(s.marketAddr1, "market.gonna.have.it", s.addr5) + s.requireFundAccount(s.addr1, "70pear,100fig") + s.requireFundAccount(s.addr2, "10apple,100fig") + s.requireFundAccount(s.addr3, "3apple,100fig") + s.requireAddFinalizeAndActivateMarker(s.coin("13apple"), s.addr5, "*.gonna.have.it") + s.requireAddFinalizeAndActivateMarker(s.coin("70pear"), s.addr5, "*.gonna.have.it") + s.requireAddFinalizeAndActivateMarker(s.coin("300fig"), s.addr5, "*.gonna.have.it") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AcceptingOrders: true, AllowUserSettlement: true, + FeeCreateAskFlat: s.coins("200fig"), + FeeCreateBidFlat: s.coins("10fig"), + FeeSellerSettlementFlat: s.coins("5pear,12fig"), + FeeSellerSettlementRatios: s.ratios("35pear:2pear"), + FeeBuyerSettlementFlat: s.coins("30fig"), + FeeBuyerSettlementRatios: s.ratios("10pear:1fig"), + ReqAttrCreateAsk: []string{"not.gonna.have.it"}, + ReqAttrCreateBid: []string{"*.gonna.have.it"}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(12345).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("50pear"), + SellerSettlementFlatFee: s.coinP("5pear"), ExternalId: "first order", + })) + s.requireAddHold(s.addr2, "10apple", 12345) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(98765).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr3.String(), Assets: s.coin("3apple"), Price: s.coin("20pear"), + SellerSettlementFlatFee: s.coinP("12fig"), ExternalId: "second order", + })) + s.requireAddHold(s.addr3, "3apple,12fig", 98765) + }, + msg: exchange.MsgFillAsksRequest{ + Buyer: s.addr1.String(), + MarketId: 1, + TotalPrice: s.coin("70pear"), + AskOrderIds: []uint64{12345, 98765}, + BuyerSettlementFees: s.coins("37fig"), + BidOrderCreationFee: s.coinP("10fig"), + }, + fArgs: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.coin("13apple"), s.zeroCoin("pear"), s.coin("53fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("42pear"), s.coin("100fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr3, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("18pear"), s.coin("88fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.marketAddr1, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("9pear"), s.coin("55fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.feeCollectorAddr, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("1pear"), s.coin("4fig")}, + }, + }, + expEvents: sdk.Events{ + // Hold release events. + s.eventHoldReleased(s.addr2, "10apple"), + s.eventHoldReleased(s.addr3, "3apple,12fig"), + + // Asset transfer events. + s.eventCoinSpent(s.addr2, "10apple"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr3, "3apple"), + s.eventMessageSender(s.addr3), + s.eventCoinReceived(s.addr1, "13apple"), + s.eventTransfer(s.addr1, nil, "13apple"), + + // Price transfer events. + s.eventCoinSpent(s.addr1, "70pear"), + s.eventMessageSender(s.addr1), + s.eventCoinReceived(s.addr2, "50pear"), + s.eventTransfer(s.addr2, nil, "50pear"), + s.eventCoinReceived(s.addr3, "20pear"), + s.eventTransfer(s.addr3, nil, "20pear"), + + // Settlement fee transfer events. + s.eventCoinSpent(s.addr2, "8pear"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr3, "12fig,2pear"), + s.eventMessageSender(s.addr3), + s.eventCoinSpent(s.addr1, "37fig"), + s.eventMessageSender(s.addr1), + s.eventCoinReceived(s.marketAddr1, "49fig,10pear"), + s.eventTransfer(s.marketAddr1, nil, "49fig,10pear"), + + // Transfer of exchange portion of settlement fee. + s.eventCoinSpent(s.marketAddr1, "3fig,1pear"), + s.eventCoinReceived(s.feeCollectorAddr, "3fig,1pear"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr1, "3fig,1pear"), + s.eventMessageSender(s.marketAddr1), + + // Order filled events. + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 12345, + Assets: "10apple", + Price: "50pear", + Fees: "8pear", + MarketId: 1, + ExternalId: "first order", + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 98765, + Assets: "3apple", + Price: "20pear", + Fees: "12fig,2pear", + MarketId: 1, + ExternalId: "second order", + }), + + // Order creation fee events. + s.eventCoinSpent(s.addr1, "10fig"), + s.eventCoinReceived(s.marketAddr1, "10fig"), + s.eventTransfer(s.marketAddr1, s.addr1, "10fig"), + s.eventMessageSender(s.addr1), + + // Transfer of exchange portion of order creation fees. + s.eventCoinSpent(s.marketAddr1, "1fig"), + s.eventCoinReceived(s.feeCollectorAddr, "1fig"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr1, "1fig"), + s.eventMessageSender(s.marketAddr1), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketSettle() { + type followupArgs struct { + expBals []expBalances + partialLeft *exchange.Order + } + testDef := msgServerTestDef[exchange.MsgMarketSettleRequest, exchange.MsgMarketSettleResponse, followupArgs]{ + endpointName: "MarketSettle", + endpoint: keeper.NewMsgServer(s.k).MarketSettle, + expResp: &exchange.MsgMarketSettleResponse{}, + followup: func(msg *exchange.MsgMarketSettleRequest, fArgs followupArgs) { + for _, orderID := range msg.AskOrderIds { + var expOrder *exchange.Order + if fArgs.partialLeft != nil && fArgs.partialLeft.OrderId == orderID { + expOrder = fArgs.partialLeft + } + order, err := s.k.GetOrder(s.ctx, orderID) + s.Assert().NoError(err, "GetOrder(%d) error", orderID) + s.Assert().Equal(expOrder, order, "GetOrder(%d) order", orderID) + } + + for _, eb := range fArgs.expBals { + s.checkBalances(eb) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketSettleRequest, followupArgs]{ + { + name: "admin does not have settle permission", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_settle)}, + }) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 1, + AskOrderIds: []uint64{1}, + BidOrderIds: []uint64{2}, + ExpectPartial: false, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to settle orders for market 1"}, + }, + { + name: "an address is sanctioned", + setup: func() { + s.requireFundAccount(s.addr1, "7apple") + s.requireFundAccount(s.addr2, "100pear") + s.requireFundAccount(s.addr3, "11apple") + s.requireFundAccount(s.addr4, "85pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + })) + s.requireAddHold(s.addr1, "7apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + })) + s.requireAddHold(s.addr2, "100pear", 22) + s.requireSetOrderInStore(store, exchange.NewOrder(333).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr3.String(), Assets: s.coin("11apple"), Price: s.coin("105pear"), + })) + s.requireAddHold(s.addr3, "11apple", 333) + s.requireSetOrderInStore(store, exchange.NewOrder(4444).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr4.String(), Assets: s.coin("8apple"), Price: s.coin("85pear"), + })) + s.requireAddHold(s.addr4, "85pear", 4444) + + s.requireSanctionAddress(s.addr2) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 1, + AskOrderIds: []uint64{333, 1}, + BidOrderIds: []uint64{22, 4444}, + }, + expInErr: []string{invReqErr, "cannot send from " + s.addr2.String(), "account is sanctioned"}, + }, + { + name: "a buyer does not have asset req attr", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("18apple"), s.addr5, "*.have.it") + s.requireSetNameRecord("buyer.have.it", s.addr5) + s.requireSetNameRecord("seller.have.it", s.addr5) + s.requireSetNameRecord("doesnot-have.it", s.addr5) + s.requireSetAttr(s.addr1, "seller.have.it", s.addr5) + s.requireSetAttr(s.addr2, "buyer.have.it", s.addr5) + s.requireSetAttr(s.addr3, "seller.have.it", s.addr5) + s.requireSetAttr(s.addr4, "doesnot-have.it", s.addr5) + s.requireFundAccount(s.addr1, "7apple") + s.requireFundAccount(s.addr2, "100pear") + s.requireFundAccount(s.addr3, "11apple") + s.requireFundAccount(s.addr4, "85pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + })) + s.requireAddHold(s.addr1, "7apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + })) + s.requireAddHold(s.addr2, "100pear", 22) + s.requireSetOrderInStore(store, exchange.NewOrder(333).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr3.String(), Assets: s.coin("11apple"), Price: s.coin("105pear"), + })) + s.requireAddHold(s.addr3, "11apple", 333) + s.requireSetOrderInStore(store, exchange.NewOrder(4444).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr4.String(), Assets: s.coin("8apple"), Price: s.coin("85pear"), + })) + s.requireAddHold(s.addr4, "85pear", 4444) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 1, + AskOrderIds: []uint64{333, 1}, + BidOrderIds: []uint64{22, 4444}, + }, + expInErr: []string{invReqErr, + "address " + s.addr4.String() + " does not contain the \"apple\" required attribute: \"*.have.it\""}, + }, + { + name: "a seller does not have price req attr", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("185pear"), s.addr5, "*.have.it") + s.requireSetNameRecord("buyer.have.it", s.addr5) + s.requireSetNameRecord("seller.have.it", s.addr5) + s.requireSetNameRecord("doesnot-have.it", s.addr5) + s.requireSetAttr(s.addr1, "doesnot-have.it", s.addr5) + s.requireSetAttr(s.addr2, "buyer.have.it", s.addr5) + s.requireSetAttr(s.addr3, "seller.have.it", s.addr5) + s.requireSetAttr(s.addr4, "buyer.have.it", s.addr5) + s.requireFundAccount(s.addr1, "7apple") + s.requireFundAccount(s.addr2, "100pear") + s.requireFundAccount(s.addr3, "11apple") + s.requireFundAccount(s.addr4, "85pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + })) + s.requireAddHold(s.addr1, "7apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + })) + s.requireAddHold(s.addr2, "100pear", 22) + s.requireSetOrderInStore(store, exchange.NewOrder(333).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr3.String(), Assets: s.coin("11apple"), Price: s.coin("105pear"), + })) + s.requireAddHold(s.addr3, "11apple", 333) + s.requireSetOrderInStore(store, exchange.NewOrder(4444).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr4.String(), Assets: s.coin("8apple"), Price: s.coin("85pear"), + })) + s.requireAddHold(s.addr4, "85pear", 4444) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 1, + AskOrderIds: []uint64{333, 1}, + BidOrderIds: []uint64{22, 4444}, + }, + expInErr: []string{invReqErr, + "address " + s.addr1.String() + " does not contain the \"pear\" required attribute: \"*.have.it\""}, + }, + { + name: "all addresses quarantined", + setup: func() { + s.requireFundAccount(s.addr1, "7apple") + s.requireFundAccount(s.addr2, "100pear") + s.requireFundAccount(s.addr3, "11apple") + s.requireFundAccount(s.addr4, "85pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + })) + s.requireAddHold(s.addr1, "7apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + })) + s.requireAddHold(s.addr2, "100pear", 22) + s.requireSetOrderInStore(store, exchange.NewOrder(333).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr3.String(), Assets: s.coin("11apple"), Price: s.coin("105pear"), + })) + s.requireAddHold(s.addr3, "11apple", 333) + s.requireSetOrderInStore(store, exchange.NewOrder(4444).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr4.String(), Assets: s.coin("8apple"), Price: s.coin("85pear"), + })) + s.requireAddHold(s.addr4, "85pear", 4444) + + s.requireQuarantineOptIn(s.addr1) + s.requireQuarantineOptIn(s.addr2) + s.requireQuarantineOptIn(s.addr3) + s.requireQuarantineOptIn(s.addr4) + s.requireQuarantineOptIn(s.addr5) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 1, + AskOrderIds: []uint64{1, 333}, + BidOrderIds: []uint64{4444, 22}, + }, + fArgs: followupArgs{ + expBals: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("77pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.coin("10apple"), s.zeroCoin("pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + { + addr: s.addr3, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("108pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + { + addr: s.addr4, + expBal: []sdk.Coin{s.coin("8apple"), s.zeroCoin("pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + }, + }, + expEvents: sdk.Events{ + // Hold releases + s.eventHoldReleased(s.addr1, "7apple"), + s.eventHoldReleased(s.addr3, "11apple"), + s.eventHoldReleased(s.addr4, "85pear"), + s.eventHoldReleased(s.addr2, "100pear"), + + // Asset transfers + s.eventCoinSpent(s.addr1, "7apple"), + s.eventCoinReceived(s.addr4, "7apple"), + s.eventTransfer(s.addr4, s.addr1, "7apple"), + s.eventMessageSender(s.addr1), + s.eventCoinSpent(s.addr3, "11apple"), + s.eventMessageSender(s.addr3), + s.eventCoinReceived(s.addr4, "1apple"), + s.eventTransfer(s.addr4, nil, "1apple"), + s.eventCoinReceived(s.addr2, "10apple"), + s.eventTransfer(s.addr2, nil, "10apple"), + + // Price transfers + s.eventCoinSpent(s.addr4, "85pear"), + s.eventMessageSender(s.addr4), + s.eventCoinReceived(s.addr1, "75pear"), + s.eventTransfer(s.addr1, nil, "75pear"), + s.eventCoinReceived(s.addr3, "10pear"), + s.eventTransfer(s.addr3, nil, "10pear"), + s.eventCoinSpent(s.addr2, "100pear"), + s.eventMessageSender(s.addr2), + s.eventCoinReceived(s.addr3, "98pear"), + s.eventTransfer(s.addr3, nil, "98pear"), + s.eventCoinReceived(s.addr1, "2pear"), + s.eventTransfer(s.addr1, nil, "2pear"), + + // Orders filled + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 1, Assets: "7apple", Price: "77pear", MarketId: 1, + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 333, Assets: "11apple", Price: "108pear", MarketId: 1, + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 4444, Assets: "8apple", Price: "85pear", MarketId: 1, + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 22, Assets: "10apple", Price: "100pear", MarketId: 1, + }), + }, + }, + { + name: "one ask, one bid, partial ask", + setup: func() { + s.requireFundAccount(s.addr1, "10apple") + s.requireFundAccount(s.addr2, "75pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + AllowPartial: true, + })) + s.requireAddHold(s.addr1, "10apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 3, Buyer: s.addr2.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + })) + s.requireAddHold(s.addr2, "75pear", 22) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AskOrderIds: []uint64{1}, + BidOrderIds: []uint64{22}, + ExpectPartial: true, + }, + fArgs: followupArgs{ + expBals: []expBalances{ + { + addr: s.addr1, + expBal: s.coins("3apple,75pear"), + expHold: []sdk.Coin{s.coin("3apple"), s.zeroCoin("pear")}, + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.coin("7apple"), s.zeroCoin("pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + }, + partialLeft: exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), Assets: s.coin("3apple"), Price: s.coin("30pear"), + AllowPartial: true, + }), + }, + expEvents: sdk.Events{ + // Hold releases + s.eventHoldReleased(s.addr2, "75pear"), + s.eventHoldReleased(s.addr1, "7apple"), + + // Asset transfer + s.eventCoinSpent(s.addr1, "7apple"), + s.eventCoinReceived(s.addr2, "7apple"), + s.eventTransfer(s.addr2, s.addr1, "7apple"), + s.eventMessageSender(s.addr1), + + // Price transfer + s.eventCoinSpent(s.addr2, "75pear"), + s.eventCoinReceived(s.addr1, "75pear"), + s.eventTransfer(s.addr1, s.addr2, "75pear"), + s.eventMessageSender(s.addr2), + + // Orders filled + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 22, Assets: "7apple", Price: "75pear", MarketId: 3, + }), + // Partial fill + s.untypeEvent(&exchange.EventOrderPartiallyFilled{ + OrderId: 1, Assets: "7apple", Price: "75pear", MarketId: 3, + }), + }, + }, + { + name: "one ask, one bid, partial bid", + setup: func() { + s.requireFundAccount(s.addr1, "7apple") + s.requireFundAccount(s.addr2, "100pear") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 3, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("65pear"), + })) + s.requireAddHold(s.addr1, "7apple", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 3, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + AllowPartial: true, + })) + s.requireAddHold(s.addr2, "100pear", 22) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AskOrderIds: []uint64{1}, + BidOrderIds: []uint64{22}, + ExpectPartial: true, + }, + fArgs: followupArgs{ + expBals: []expBalances{ + { + addr: s.addr1, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("70pear")}, + expHold: s.zeroCoins("apple", "pear"), + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.coin("7apple"), s.coin("30pear")}, + expHold: []sdk.Coin{s.zeroCoin("apple"), s.coin("30pear")}, + }, + }, + partialLeft: exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 3, Buyer: s.addr2.String(), Assets: s.coin("3apple"), Price: s.coin("30pear"), + AllowPartial: true, + }), + }, + expEvents: sdk.Events{ + // Hold releases + s.eventHoldReleased(s.addr1, "7apple"), + s.eventHoldReleased(s.addr2, "70pear"), + + // Asset transfer + s.eventCoinSpent(s.addr1, "7apple"), + s.eventCoinReceived(s.addr2, "7apple"), + s.eventTransfer(s.addr2, s.addr1, "7apple"), + s.eventMessageSender(s.addr1), + + // Price transfer + s.eventCoinSpent(s.addr2, "70pear"), + s.eventCoinReceived(s.addr1, "70pear"), + s.eventTransfer(s.addr1, s.addr2, "70pear"), + s.eventMessageSender(s.addr2), + + // Orders filled + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 1, Assets: "7apple", Price: "70pear", MarketId: 3, + }), + // Partial fill + s.untypeEvent(&exchange.EventOrderPartiallyFilled{ + OrderId: 22, Assets: "7apple", Price: "70pear", MarketId: 3, + }), + }, + }, + { + name: "two of each with fees and req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("185pear"), s.addr5, "*.have.it") + s.requireAddFinalizeAndActivateMarker(s.coin("18apple"), s.addr5, "*.have.it") + s.requireSetNameRecord("buyer.have.it", s.addr5) + s.requireSetNameRecord("seller.have.it", s.addr5) + s.requireSetNameRecord("market.have.it", s.addr5) + s.requireSetAttr(s.addr1, "seller.have.it", s.addr5) + s.requireSetAttr(s.addr2, "buyer.have.it", s.addr5) + s.requireSetAttr(s.addr3, "seller.have.it", s.addr5) + s.requireSetAttr(s.addr4, "buyer.have.it", s.addr5) + s.requireSetAttr(s.marketAddr2, "market.have.it", s.addr5) + s.requireFundAccount(s.addr1, "20apple,100pear,100fig") + s.requireFundAccount(s.addr2, "20apple,100pear,100fig") + s.requireFundAccount(s.addr3, "20apple,100pear,100fig") + s.requireFundAccount(s.addr4, "20apple,100pear,100fig") + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_settle)}, + FeeSellerSettlementRatios: s.ratios("10pear:1pear"), + }) + + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr1.String(), Assets: s.coin("7apple"), Price: s.coin("75pear"), + SellerSettlementFlatFee: s.coinP("10fig"), + })) + s.requireAddHold(s.addr1, "7apple,10fig", 1) + s.requireSetOrderInStore(store, exchange.NewOrder(22).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr2.String(), Assets: s.coin("10apple"), Price: s.coin("100pear"), + BuyerSettlementFees: s.coins("20fig"), + })) + s.requireAddHold(s.addr2, "100pear,20fig", 22) + s.requireSetOrderInStore(store, exchange.NewOrder(333).WithAsk(&exchange.AskOrder{ + MarketId: 2, Seller: s.addr3.String(), Assets: s.coin("11apple"), Price: s.coin("105pear"), + SellerSettlementFlatFee: s.coinP("5pear"), + })) + s.requireAddHold(s.addr3, "11apple", 333) + s.requireSetOrderInStore(store, exchange.NewOrder(4444).WithBid(&exchange.BidOrder{ + MarketId: 2, Buyer: s.addr4.String(), Assets: s.coin("8apple"), Price: s.coin("85pear"), + BuyerSettlementFees: s.coins("10pear"), + })) + s.requireAddHold(s.addr4, "95pear", 4444) + }, + msg: exchange.MsgMarketSettleRequest{ + Admin: s.addr5.String(), + MarketId: 2, + AskOrderIds: []uint64{1, 333}, + BidOrderIds: []uint64{22, 4444}, + }, + fArgs: followupArgs{ + expBals: []expBalances{ + { + addr: s.addr1, + expBal: s.coins("13apple,169pear,90fig"), + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr2, + expBal: []sdk.Coin{s.coin("30apple"), s.zeroCoin("pear"), s.coin("80fig")}, + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr3, + expBal: s.coins("9apple,192pear,100fig"), + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.addr4, + expBal: s.coins("28apple,5pear,100fig"), + expHold: s.zeroCoins("apple", "pear", "fig"), + }, + { + addr: s.marketAddr2, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("32pear"), s.coin("28fig")}, + }, + { + addr: s.feeCollectorAddr, + expBal: []sdk.Coin{s.zeroCoin("apple"), s.coin("2pear"), s.coin("2fig")}, + }, + }, + }, + expEvents: sdk.Events{ + // Hold releases + s.eventHoldReleased(s.addr1, "7apple,10fig"), + s.eventHoldReleased(s.addr3, "11apple"), + s.eventHoldReleased(s.addr2, "20fig,100pear"), + s.eventHoldReleased(s.addr4, "95pear"), + + // Asset transfers + s.eventCoinSpent(s.addr1, "7apple"), + s.eventCoinReceived(s.addr2, "7apple"), + s.eventTransfer(s.addr2, s.addr1, "7apple"), + s.eventMessageSender(s.addr1), + s.eventCoinSpent(s.addr3, "11apple"), + s.eventMessageSender(s.addr3), + s.eventCoinReceived(s.addr2, "3apple"), + s.eventTransfer(s.addr2, nil, "3apple"), + s.eventCoinReceived(s.addr4, "8apple"), + s.eventTransfer(s.addr4, nil, "8apple"), + + // Price transfers + s.eventCoinSpent(s.addr2, "100pear"), + s.eventMessageSender(s.addr2), + s.eventCoinReceived(s.addr1, "75pear"), + s.eventTransfer(s.addr1, nil, "75pear"), + s.eventCoinReceived(s.addr3, "25pear"), + s.eventTransfer(s.addr3, nil, "25pear"), + s.eventCoinSpent(s.addr4, "85pear"), + s.eventMessageSender(s.addr4), + s.eventCoinReceived(s.addr3, "83pear"), + s.eventTransfer(s.addr3, nil, "83pear"), + s.eventCoinReceived(s.addr1, "2pear"), + s.eventTransfer(s.addr1, nil, "2pear"), + + // Fee transfers to market + s.eventCoinSpent(s.addr1, "10fig,8pear"), + s.eventMessageSender(s.addr1), + s.eventCoinSpent(s.addr3, "16pear"), + s.eventMessageSender(s.addr3), + s.eventCoinSpent(s.addr2, "20fig"), + s.eventMessageSender(s.addr2), + s.eventCoinSpent(s.addr4, "10pear"), + s.eventMessageSender(s.addr4), + s.eventCoinReceived(s.marketAddr2, "30fig,34pear"), + s.eventTransfer(s.marketAddr2, nil, "30fig,34pear"), + + // Transfers of exchange portion of fees + s.eventCoinSpent(s.marketAddr2, "2fig,2pear"), + s.eventCoinReceived(s.feeCollectorAddr, "2fig,2pear"), + s.eventTransfer(s.feeCollectorAddr, s.marketAddr2, "2fig,2pear"), + s.eventMessageSender(s.marketAddr2), + + // Orders filled + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 1, Assets: "7apple", Price: "77pear", MarketId: 2, Fees: "10fig,8pear", + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 333, Assets: "11apple", Price: "108pear", MarketId: 2, Fees: "16pear", + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 22, Assets: "10apple", Price: "100pear", MarketId: 2, Fees: "20fig", + }), + s.untypeEvent(&exchange.EventOrderFilled{ + OrderId: 4444, Assets: "8apple", Price: "85pear", MarketId: 2, Fees: "10pear", + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketSetOrderExternalID() { + type followupArgs struct{} + testDef := msgServerTestDef[exchange.MsgMarketSetOrderExternalIDRequest, exchange.MsgMarketSetOrderExternalIDResponse, followupArgs]{ + endpointName: "MarketSetOrderExternalID", + endpoint: keeper.NewMsgServer(s.k).MarketSetOrderExternalID, + expResp: &exchange.MsgMarketSetOrderExternalIDResponse{}, + followup: func(msg *exchange.MsgMarketSetOrderExternalIDRequest, _ followupArgs) { + order, err := s.k.GetOrder(s.ctx, msg.OrderId) + s.Assert().NoError(err, "GetOrder(%d) error", msg.OrderId) + if s.Assert().NotNil(order, "GetOrder(%d) order", msg.OrderId) { + s.Assert().Equal(msg.ExternalId, order.GetExternalID(), "GetOrder(%d) order ExternalID", msg.OrderId) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketSetOrderExternalIDRequest, followupArgs]{ + { + name: "admin does not have permission", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_set_ids)}, + }) + }, + msg: exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: s.addr5.String(), MarketId: 1, OrderId: 1, ExternalId: "bananas", + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to set external ids on orders for market 1"}, + }, + { + name: "order does not exist", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_set_ids)}, + }) + }, + msg: exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: s.addr5.String(), MarketId: 1, OrderId: 1, ExternalId: "bananas", + }, + expInErr: []string{invReqErr, "order 1 not found"}, + }, + { + name: "okay: nothing to something", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_set_ids)}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 1, Seller: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + ExternalId: "", + })) + }, + msg: exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: s.addr5.String(), MarketId: 1, OrderId: 7, ExternalId: "bananas", + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventOrderExternalIDUpdated{ + OrderId: 7, + MarketId: 1, + ExternalId: "bananas", + }), + }, + }, + { + name: "okay: something to something else", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_set_ids)}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(7).WithBid(&exchange.BidOrder{ + MarketId: 1, Buyer: s.addr1.String(), Assets: s.coin("1apple"), Price: s.coin("1pear"), + ExternalId: "something", + })) + }, + msg: exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: s.addr5.String(), MarketId: 1, OrderId: 7, ExternalId: "bananas", + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventOrderExternalIDUpdated{ + OrderId: 7, + MarketId: 1, + ExternalId: "bananas", + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketWithdraw() { + testDef := msgServerTestDef[exchange.MsgMarketWithdrawRequest, exchange.MsgMarketWithdrawResponse, []expBalances]{ + endpointName: "MarketWithdraw", + endpoint: keeper.NewMsgServer(s.k).MarketWithdraw, + expResp: &exchange.MsgMarketWithdrawResponse{}, + followup: func(_ *exchange.MsgMarketWithdrawRequest, fArgs []expBalances) { + for _, eb := range fArgs { + s.checkBalances(eb) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketWithdrawRequest, []expBalances]{ + { + name: "admin does not have permission to withdraw", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_withdraw)}, + }) + }, + msg: exchange.MsgMarketWithdrawRequest{ + Admin: s.addr5.String(), MarketId: 1, ToAddress: s.addr1.String(), Amount: s.coins("100fig"), + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to withdraw from market 1"}, + }, + { + name: "insufficient funds in market", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_withdraw)}, + }) + s.requireFundAccount(s.marketAddr1, "100apple,99pear,100fig") + }, + msg: exchange.MsgMarketWithdrawRequest{ + Admin: s.addr5.String(), MarketId: 1, ToAddress: s.addr1.String(), Amount: s.coins("3apple,100pear,50fig"), + }, + expInErr: []string{invReqErr, "spendable balance 99pear is smaller than 100pear", "insufficient funds"}, + }, + { + name: "destination does not have req attrs", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("105apple"), s.addr5, "*.apple.what.what") + s.requireAddFinalizeAndActivateMarker(s.coin("105pear"), s.addr5, "*.pear.what.what") + s.requireSetNameRecord("nut.apple.what.what", s.addr5) + s.requireSetNameRecord("nut.pear.what.what", s.addr5) + s.requireSetAttr(s.marketAddr1, "nut.apple.what.what", s.addr5) + s.requireSetAttr(s.marketAddr1, "nut.pear.what.what", s.addr5) + s.requireSetAttr(s.addr1, "nut.apple.what.what", s.addr5) + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_withdraw)}, + }) + s.requireFundAccount(s.marketAddr1, "100apple,100pear,100fig") + }, + msg: exchange.MsgMarketWithdrawRequest{ + Admin: s.addr5.String(), MarketId: 1, ToAddress: s.addr1.String(), Amount: s.coins("3apple,100pear,50fig"), + }, + expInErr: []string{invReqErr, "failed to withdraw 3apple,50fig,100pear from market 1", + "address " + s.addr1.String() + " does not contain the \"pear\" required attribute: \"*.pear.what.what\""}, + }, + { + name: "okay", + setup: func() { + s.requireAddFinalizeAndActivateMarker(s.coin("100apple"), s.addr5, "*.apple.what.what") + s.requireAddFinalizeAndActivateMarker(s.coin("100pear"), s.addr5, "*.pear.what.what") + s.requireSetNameRecord("nut.apple.what.what", s.addr5) + s.requireSetNameRecord("nut.pear.what.what", s.addr5) + s.requireSetAttr(s.marketAddr1, "nut.apple.what.what", s.addr5) + s.requireSetAttr(s.marketAddr1, "nut.pear.what.what", s.addr5) + s.requireSetAttr(s.addr1, "nut.apple.what.what", s.addr5) + s.requireSetAttr(s.addr1, "nut.pear.what.what", s.addr5) + + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_withdraw)}, + }) + s.requireFundAccount(s.marketAddr1, "100apple,100pear,100fig") + s.requireFundAccount(s.addr1, "5apple,5pear") + }, + msg: exchange.MsgMarketWithdrawRequest{ + Admin: s.addr5.String(), MarketId: 1, ToAddress: s.addr1.String(), Amount: s.coins("3apple,100pear,50fig"), + }, + fArgs: []expBalances{ + { + addr: s.marketAddr1, + expBal: []sdk.Coin{s.coin("97apple"), s.zeroCoin("pear"), s.coin("50fig")}, + }, + { + addr: s.addr1, + expBal: s.coins("8apple,105pear,50fig"), + }, + }, + expEvents: sdk.Events{ + s.eventCoinSpent(s.marketAddr1, "3apple,50fig,100pear"), + s.eventCoinReceived(s.addr1, "3apple,50fig,100pear"), + s.eventTransfer(s.addr1, s.marketAddr1, "3apple,50fig,100pear"), + s.eventMessageSender(s.marketAddr1), + s.untypeEvent(&exchange.EventMarketWithdraw{ + MarketId: 1, + Amount: "3apple,50fig,100pear", + Destination: s.addr1.String(), + WithdrawnBy: s.addr5.String(), + }), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketUpdateDetails() { + testDef := msgServerTestDef[exchange.MsgMarketUpdateDetailsRequest, exchange.MsgMarketUpdateDetailsResponse, struct{}]{ + endpointName: "MarketUpdateDetails", + endpoint: keeper.NewMsgServer(s.k).MarketUpdateDetails, + expResp: &exchange.MsgMarketUpdateDetailsResponse{}, + followup: func(msg *exchange.MsgMarketUpdateDetailsRequest, _ struct{}) { + market := s.k.GetMarket(s.ctx, msg.MarketId) + if s.Assert().NotNil(market, "GetMarket(%d)", msg.MarketId) { + s.Assert().Equal(msg.MarketDetails, market.MarketDetails, "market %d details", msg.MarketId) + } + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketUpdateDetailsRequest, struct{}]{ + { + name: "admin does not have permission to update market", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_update)}, + }) + }, + msg: exchange.MsgMarketUpdateDetailsRequest{ + Admin: s.addr5.String(), + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "new name"}, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to update market 2"}, + }, + { + name: "error updating details", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + }) + ma := s.k.GetMarketAccount(s.ctx, 2) + s.app.AccountKeeper.SetAccount(s.ctx, ma.BaseAccount) + }, + msg: exchange.MsgMarketUpdateDetailsRequest{ + Admin: s.addr5.String(), + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "new name"}, + }, + expInErr: []string{invReqErr, "market 2 account not found"}, + }, + { + name: "all good", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + MarketDetails: exchange.MarketDetails{ + Name: "Market 2 Old Name", + Description: "The old description of market 2.", + WebsiteUrl: "http://example.com/old/market/2", + IconUri: "http://oops.example.com/old/market/2", + }, + }) + }, + msg: exchange.MsgMarketUpdateDetailsRequest{ + Admin: s.addr5.String(), + MarketId: 2, + MarketDetails: exchange.MarketDetails{ + Name: "Market Two", + Description: "This is the new, better, stronger description of Market Two!", + WebsiteUrl: "http://example.com/new/market/2", + IconUri: "http://example.com/new/market/2/icon", + }, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketDetailsUpdated{MarketId: 2, UpdatedBy: s.addr5.String()}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketUpdateEnabled() { + testDef := msgServerTestDef[exchange.MsgMarketUpdateEnabledRequest, exchange.MsgMarketUpdateEnabledResponse, struct{}]{ + endpointName: "MarketUpdateEnabled", + endpoint: keeper.NewMsgServer(s.k).MarketUpdateEnabled, + expResp: &exchange.MsgMarketUpdateEnabledResponse{}, + followup: func(msg *exchange.MsgMarketUpdateEnabledRequest, _ struct{}) { + isEnabled := s.k.IsMarketActive(s.ctx, msg.MarketId) + s.Assert().Equal(msg.AcceptingOrders, isEnabled, "IsMarketActive(%d)", msg.MarketId) + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketUpdateEnabledRequest, struct{}]{ + { + name: "admin does not have permission to update market", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_update)}, + }) + }, + msg: exchange.MsgMarketUpdateEnabledRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AcceptingOrders: true, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to update market 3"}, + }, + { + name: "false to false", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AcceptingOrders: false, + }) + }, + msg: exchange.MsgMarketUpdateEnabledRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AcceptingOrders: false, + }, + expInErr: []string{invReqErr, "market 3 already has accepting-orders false"}, + }, + { + name: "true to true", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AcceptingOrders: true, + }) + }, + msg: exchange.MsgMarketUpdateEnabledRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AcceptingOrders: true, + }, + expInErr: []string{invReqErr, "market 3 already has accepting-orders true"}, + }, + { + name: "false to true", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AcceptingOrders: false, + }) + }, + msg: exchange.MsgMarketUpdateEnabledRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AcceptingOrders: true, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketEnabled{MarketId: 3, UpdatedBy: s.addr5.String()}), + }, + }, + { + name: "true to false", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AcceptingOrders: true, + }) + }, + msg: exchange.MsgMarketUpdateEnabledRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AcceptingOrders: false, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketDisabled{MarketId: 3, UpdatedBy: s.addr5.String()}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketUpdateUserSettle() { + testDef := msgServerTestDef[exchange.MsgMarketUpdateUserSettleRequest, exchange.MsgMarketUpdateUserSettleResponse, struct{}]{ + endpointName: "MarketUpdateUserSettle", + endpoint: keeper.NewMsgServer(s.k).MarketUpdateUserSettle, + expResp: &exchange.MsgMarketUpdateUserSettleResponse{}, + followup: func(msg *exchange.MsgMarketUpdateUserSettleRequest, _ struct{}) { + allowed := s.k.IsUserSettlementAllowed(s.ctx, msg.MarketId) + s.Assert().Equal(msg.AllowUserSettlement, allowed, "IsUserSettlementAllowed(%d)", msg.MarketId) + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketUpdateUserSettleRequest, struct{}]{ + { + name: "admin does not have permission to update market", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_update)}, + }) + }, + msg: exchange.MsgMarketUpdateUserSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AllowUserSettlement: true, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to update market 3"}, + }, + { + name: "false to false", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AllowUserSettlement: false, + }) + }, + msg: exchange.MsgMarketUpdateUserSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AllowUserSettlement: false, + }, + expInErr: []string{invReqErr, "market 3 already has allow-user-settlement false"}, + }, + { + name: "true to true", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AllowUserSettlement: true, + }) + }, + msg: exchange.MsgMarketUpdateUserSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AllowUserSettlement: true, + }, + expInErr: []string{invReqErr, "market 3 already has allow-user-settlement true"}, + }, + { + name: "false to true", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AllowUserSettlement: false, + }) + }, + msg: exchange.MsgMarketUpdateUserSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AllowUserSettlement: true, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketUserSettleEnabled{MarketId: 3, UpdatedBy: s.addr5.String()}), + }, + }, + { + name: "true to false", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 3, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_update)}, + AllowUserSettlement: true, + }) + }, + msg: exchange.MsgMarketUpdateUserSettleRequest{ + Admin: s.addr5.String(), + MarketId: 3, + AllowUserSettlement: false, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketUserSettleDisabled{MarketId: 3, UpdatedBy: s.addr5.String()}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketManagePermissions() { + testDef := msgServerTestDef[exchange.MsgMarketManagePermissionsRequest, exchange.MsgMarketManagePermissionsResponse, []exchange.AccessGrant]{ + endpointName: "MarketManagePermissions", + endpoint: keeper.NewMsgServer(s.k).MarketManagePermissions, + expResp: &exchange.MsgMarketManagePermissionsResponse{}, + followup: func(msg *exchange.MsgMarketManagePermissionsRequest, expAGs []exchange.AccessGrant) { + for _, expAG := range expAGs { + addr, err := sdk.AccAddressFromBech32(expAG.Address) + if s.Assert().NoError(err, "AccAddressFromBech32(%q)", expAG.Address) { + actPerms := s.k.GetUserPermissions(s.ctx, msg.MarketId, addr) + s.Assert().Equal(expAG.Permissions, actPerms, "market %d permissions for %s", msg.MarketId, s.getAddrName(addr)) + } + + } + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketManagePermissionsRequest, []exchange.AccessGrant]{ + { + name: "admin does not have permission to manage permissions", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_permissions)}, + }) + }, + msg: exchange.MsgMarketManagePermissionsRequest{ + Admin: s.addr5.String(), + MarketId: 1, + RevokeAll: []string{s.addr1.String()}, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to manage permissions for market 1"}, + }, + { + name: "error updating permissions", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_permissions)}, + }) + }, + msg: exchange.MsgMarketManagePermissionsRequest{ + Admin: s.addr5.String(), + MarketId: 1, + RevokeAll: []string{s.addr1.String()}, + }, + expInErr: []string{invReqErr, "account " + s.addr1.String() + " does not have any permissions for market 1"}, + }, + { + name: "okay", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{ + s.agCanEverything(s.addr1), + s.agCanEverything(s.addr2), + s.agCanOnly(s.addr3, exchange.Permission_withdraw), + s.agCanOnly(s.addr5, exchange.Permission_permissions), + }, + }) + }, + msg: exchange.MsgMarketManagePermissionsRequest{ + Admin: s.addr5.String(), + MarketId: 1, + RevokeAll: []string{s.addr1.String()}, + ToRevoke: []exchange.AccessGrant{ + s.agCanAllBut(s.addr2, exchange.Permission_cancel), + s.agCanOnly(s.addr3, exchange.Permission_withdraw), + }, + ToGrant: []exchange.AccessGrant{ + s.agCanOnly(s.addr4, exchange.Permission_withdraw), + }, + }, + fArgs: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: nil}, + s.agCanOnly(s.addr2, exchange.Permission_cancel), + {Address: s.addr3.String(), Permissions: nil}, + s.agCanOnly(s.addr4, exchange.Permission_withdraw), + s.agCanOnly(s.addr5, exchange.Permission_permissions), + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketPermissionsUpdated{MarketId: 1, UpdatedBy: s.addr5.String()}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_MarketManageReqAttrs() { + type followupArgs struct { + expAsk []string + expBid []string + } + testDef := msgServerTestDef[exchange.MsgMarketManageReqAttrsRequest, exchange.MsgMarketManageReqAttrsResponse, followupArgs]{ + endpointName: "MarketManageReqAttrs", + endpoint: keeper.NewMsgServer(s.k).MarketManageReqAttrs, + expResp: &exchange.MsgMarketManageReqAttrsResponse{}, + followup: func(msg *exchange.MsgMarketManageReqAttrsRequest, fArgs followupArgs) { + actAsk := s.k.GetReqAttrsAsk(s.ctx, msg.MarketId) + actBid := s.k.GetReqAttrsBid(s.ctx, msg.MarketId) + s.Assert().Equal(fArgs.expAsk, actAsk, "market %d req attrs ask", msg.MarketId) + s.Assert().Equal(fArgs.expBid, actBid, "market %d req attrs bid", msg.MarketId) + }, + } + + tests := []msgServerTestCase[exchange.MsgMarketManageReqAttrsRequest, followupArgs]{ + { + name: "admin does not have permission to manage req attrs", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_attributes)}, + }) + }, + msg: exchange.MsgMarketManageReqAttrsRequest{ + Admin: s.addr5.String(), MarketId: 1, CreateAskToAdd: []string{"nope"}, + }, + expInErr: []string{invReqErr, + "account " + s.addr5.String() + " does not have permission to manage required attributes for market 1"}, + }, + { + name: "error updating attrs", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_attributes)}, + }) + }, + msg: exchange.MsgMarketManageReqAttrsRequest{ + Admin: s.addr5.String(), + MarketId: 1, + CreateAskToRemove: []string{"nope"}, + }, + expInErr: []string{invReqErr, + "cannot remove create-ask required attribute \"nope\": attribute not currently required"}, + }, + { + name: "okay", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_attributes)}, + ReqAttrCreateAsk: []string{"ask.base", "*.other"}, + ReqAttrCreateBid: []string{"bid.base", "*.fresh"}, + }) + }, + msg: exchange.MsgMarketManageReqAttrsRequest{ + Admin: s.addr5.String(), + MarketId: 1, + CreateAskToAdd: []string{"ask.deeper.base"}, + CreateAskToRemove: []string{"ask.base"}, + CreateBidToAdd: []string{"bid.deeper.base"}, + CreateBidToRemove: []string{"bid.base"}, + }, + fArgs: followupArgs{ + expAsk: []string{"*.other", "ask.deeper.base"}, + expBid: []string{"*.fresh", "bid.deeper.base"}, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketReqAttrUpdated{MarketId: 1, UpdatedBy: s.addr5.String()}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_GovCreateMarket() { + testDef := msgServerTestDef[exchange.MsgGovCreateMarketRequest, exchange.MsgGovCreateMarketResponse, uint32]{ + endpointName: "GovCreateMarket", + endpoint: keeper.NewMsgServer(s.k).GovCreateMarket, + expResp: &exchange.MsgGovCreateMarketResponse{}, + followup: func(msg *exchange.MsgGovCreateMarketRequest, marketID uint32) { + expMarket := msg.Market + expMarket.MarketId = marketID + actMarket := s.k.GetMarket(s.ctx, marketID) + s.Assert().Equal(expMarket, *actMarket, "GetMarket(%d)", marketID) + }, + } + + tests := []msgServerTestCase[exchange.MsgGovCreateMarketRequest, uint32]{ + { + name: "wrong authority", + msg: exchange.MsgGovCreateMarketRequest{ + Authority: s.addr5.String(), + Market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "Market 5"}}, + }, + expInErr: []string{ + "expected \"" + s.k.GetAuthority() + "\" got \"" + s.addr5.String() + "\"", + "expected gov account as only signer for proposal message"}, + }, + { + name: "error creating market", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 1, AccessGrants: []exchange.AccessGrant{s.agCanEverything(s.addr5)}, + }) + }, + msg: exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + MarketId: 1, MarketDetails: exchange.MarketDetails{Name: "Muwahahahaha"}, + }, + }, + expInErr: []string{invReqErr, "market id 1 account " + exchange.GetMarketAddress(1).String() + " already exists"}, + }, + { + name: "okay: market 0", + setup: func() { + keeper.SetLastAutoMarketID(s.getStore(), 54) + }, + msg: exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + MarketId: 0, + MarketDetails: exchange.MarketDetails{ + Name: "Next Market Please", + Description: "A description!!", + WebsiteUrl: "WeBsItEuRl", + IconUri: "iCoNuRi", + }, + FeeCreateBidFlat: s.coins("10fig"), + FeeSellerSettlementRatios: s.ratios("100apple:1apple"), + FeeBuyerSettlementFlat: s.coins("33fig"), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + s.agCanEverything(s.addr1), + s.agCanEverything(s.addr5), + }, + ReqAttrCreateAsk: []string{"*.some.thing"}, + }, + }, + fArgs: 55, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketCreated{MarketId: 55}), + }, + }, + { + name: "okay: market 420", + setup: func() { + keeper.SetLastAutoMarketID(s.getStore(), 68) + }, + msg: exchange.MsgGovCreateMarketRequest{ + Authority: s.k.GetAuthority(), + Market: exchange.Market{ + MarketId: 420, + MarketDetails: exchange.MarketDetails{ + Name: "Second Day", + Description: "It's Tuesday!", + WebsiteUrl: "websiteurl", + IconUri: "ICONURI", + }, + FeeCreateAskFlat: s.coins("10fig"), + FeeBuyerSettlementRatios: s.ratios("100apple:1apple"), + FeeSellerSettlementFlat: s.coins("33fig"), + AccessGrants: []exchange.AccessGrant{ + s.agCanEverything(s.addr4), + s.agCanOnly(s.addr5, exchange.Permission_settle), + }, + ReqAttrCreateBid: []string{"*.other.thing"}, + }, + }, + fArgs: 420, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketCreated{MarketId: 420}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_GovManageFees() { + testDef := msgServerTestDef[exchange.MsgGovManageFeesRequest, exchange.MsgGovManageFeesResponse, exchange.Market]{ + endpointName: "GovManageFees", + endpoint: keeper.NewMsgServer(s.k).GovManageFees, + expResp: &exchange.MsgGovManageFeesResponse{}, + followup: func(msg *exchange.MsgGovManageFeesRequest, expMarket exchange.Market) { + actMarket := s.k.GetMarket(s.ctx, msg.MarketId) + s.Assert().Equal(exchange.Market(expMarket), *actMarket, "GetMarket(%d)", msg.MarketId) + }, + } + + tests := []msgServerTestCase[exchange.MsgGovManageFeesRequest, exchange.Market]{ + { + name: "wrong authority", + msg: exchange.MsgGovManageFeesRequest{ + Authority: s.addr5.String(), + AddFeeCreateAskFlat: s.coins("10fig"), + }, + expInErr: []string{ + "expected \"" + s.k.GetAuthority() + "\" got \"" + s.addr5.String() + "\"", + "expected gov account as only signer for proposal message"}, + }, + { + name: "okay", + setup: func() { + s.requireCreateMarketUnmocked(exchange.Market{ + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "Market Too"}, + FeeCreateAskFlat: s.coins("9apple,5tomato"), + FeeCreateBidFlat: s.coins("9avocado,6tomato"), + FeeSellerSettlementFlat: s.coins("10apple,2tomato"), + FeeSellerSettlementRatios: s.ratios("100apple:33apple,100tomato:7tomato"), + FeeBuyerSettlementFlat: s.coins("9aubergine,1tomato"), + FeeBuyerSettlementRatios: s.ratios("100cherry:1cherry,100tomato:7tomato"), + }) + }, + msg: exchange.MsgGovManageFeesRequest{ + Authority: s.k.GetAuthority(), + MarketId: 2, + RemoveFeeCreateAskFlat: s.coins("9apple"), + AddFeeCreateAskFlat: s.coins("10apple"), + RemoveFeeCreateBidFlat: s.coins("9avocado"), + AddFeeCreateBidFlat: s.coins("10avocado"), + RemoveFeeSellerSettlementFlat: s.coins("10apple"), + AddFeeSellerSettlementFlat: s.coins("10acai"), + RemoveFeeSellerSettlementRatios: s.ratios("100apple:33apple"), + AddFeeSellerSettlementRatios: s.ratios("100acai:3acai"), + RemoveFeeBuyerSettlementFlat: s.coins("9aubergine"), + AddFeeBuyerSettlementFlat: s.coins("10aubergine"), + RemoveFeeBuyerSettlementRatios: s.ratios("100cherry:1cherry"), + AddFeeBuyerSettlementRatios: s.ratios("80cherry:3cherry"), + }, + fArgs: exchange.Market{ + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "Market Too"}, + FeeCreateAskFlat: s.coins("10apple,5tomato"), + FeeCreateBidFlat: s.coins("10avocado,6tomato"), + FeeSellerSettlementFlat: s.coins("10acai,2tomato"), + FeeSellerSettlementRatios: s.ratios("100acai:3acai,100tomato:7tomato"), + FeeBuyerSettlementFlat: s.coins("10aubergine,1tomato"), + FeeBuyerSettlementRatios: s.ratios("80cherry:3cherry,100tomato:7tomato"), + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventMarketFeesUpdated{MarketId: 2}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} + +func (s *TestSuite) TestMsgServer_GovUpdateParams() { + testDef := msgServerTestDef[exchange.MsgGovUpdateParamsRequest, exchange.MsgGovUpdateParamsResponse, struct{}]{ + endpointName: "GovUpdateParams", + endpoint: keeper.NewMsgServer(s.k).GovUpdateParams, + expResp: &exchange.MsgGovUpdateParamsResponse{}, + followup: func(msg *exchange.MsgGovUpdateParamsRequest, _ struct{}) { + actParams := s.k.GetParams(s.ctx) + s.Assert().Equal(&msg.Params, actParams, "GetParams") + }, + } + + tests := []msgServerTestCase[exchange.MsgGovUpdateParamsRequest, struct{}]{ + { + name: "wrong authority", + msg: exchange.MsgGovUpdateParamsRequest{ + Authority: s.addr5.String(), + Params: exchange.Params{}, + }, + expInErr: []string{ + "expected \"" + s.k.GetAuthority() + "\" got \"" + s.addr5.String() + "\"", + "expected gov account as only signer for proposal message"}, + }, + { + name: "okay: was not previously set", + setup: func() { + s.k.SetParams(s.ctx, nil) + }, + msg: exchange.MsgGovUpdateParamsRequest{ + Authority: s.k.GetAuthority(), + Params: exchange.Params{ + DefaultSplit: 333, + DenomSplits: []exchange.DenomSplit{{Denom: "banana", Split: 99}}, + }, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventParamsUpdated{}), + }, + }, + { + name: "okay: no change", + setup: func() { + s.k.SetParams(s.ctx, exchange.DefaultParams()) + }, + msg: exchange.MsgGovUpdateParamsRequest{ + Authority: s.k.GetAuthority(), + Params: *exchange.DefaultParams(), + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventParamsUpdated{}), + }, + }, + { + name: "okay: was previously defaults", + setup: func() { + s.k.SetParams(s.ctx, exchange.DefaultParams()) + }, + msg: exchange.MsgGovUpdateParamsRequest{ + Authority: s.k.GetAuthority(), + Params: exchange.Params{ + DefaultSplit: 333, + DenomSplits: []exchange.DenomSplit{{Denom: "banana", Split: 99}}, + }, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventParamsUpdated{}), + }, + }, + { + name: "okay: was previously set", + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 987, + DenomSplits: []exchange.DenomSplit{{Denom: "cherry", Split: 4}}, + }) + }, + msg: exchange.MsgGovUpdateParamsRequest{ + Authority: s.k.GetAuthority(), + Params: exchange.Params{ + DefaultSplit: 345, + DenomSplits: []exchange.DenomSplit{{Denom: "banana", Split: 99}}, + }, + }, + expEvents: sdk.Events{ + s.untypeEvent(&exchange.EventParamsUpdated{}), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + runMsgServerTestCase(s, testDef, tc) + }) + } +} diff --git a/x/exchange/keeper/orders.go b/x/exchange/keeper/orders.go index 5f20e1a2df..fd7828f500 100644 --- a/x/exchange/keeper/orders.go +++ b/x/exchange/keeper/orders.go @@ -5,11 +5,9 @@ import ( "fmt" "strings" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - db "github.com/tendermint/tm-db" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" @@ -109,7 +107,11 @@ func (k Keeper) parseOrderStoreKeyValue(key, value []byte) (*exchange.Order, err return nil, fmt.Errorf("invalid order store key %v: length expected to be at least 8", key) } orderID, _ := uint64FromBz(key[len(key)-8:]) - return k.parseOrderStoreValue(orderID, value) + order, err := k.parseOrderStoreValue(orderID, value) + if err != nil { + return nil, fmt.Errorf("failed to read order %d: %w", orderID, err) + } + return order, nil } // createConstantIndexEntries creates all the key/value index entries for an order that don't change. @@ -174,7 +176,7 @@ func (k Keeper) setOrderInStore(store sdk.KVStore, order exchange.Order) error { externalIDEntry := createMarketExternalIDToOrderEntry(order) if externalIDEntry != nil && store.Has(externalIDEntry.Key) { - orderIDBz := store.Get(externalIDEntry.Value) + orderIDBz := store.Get(externalIDEntry.Key) otherOrderID, ok := uint64FromBz(orderIDBz) if ok && otherOrderID != order.GetOrderID() { return fmt.Errorf("external id %q is already in use by order %d: cannot be used for order %d", @@ -226,7 +228,8 @@ func (k Keeper) iterateOrderIndex(ctx sdk.Context, prefixBz []byte, cb func(orde // getPageOfOrdersFromIndex gets a page of orders using a -to-order index. func (k Keeper) getPageOfOrdersFromIndex( - prefixStore sdk.KVStore, + ctx sdk.Context, + prefixBz []byte, pageReq *query.PageRequest, orderType string, afterOrderID uint64, @@ -245,11 +248,12 @@ func (k Keeper) getPageOfOrdersFromIndex( case exchange.OrderTypeBid: orderTypeByte = OrderKeyTypeBid default: - return nil, nil, status.Errorf(codes.InvalidArgument, "unknown order type %q", orderType) + return nil, nil, fmt.Errorf("unknown order type %q", orderType) } filterByType = true } + rootStore := k.getStore(ctx) var orders []*exchange.Order accumulator := func(key []byte, value []byte, accumulate bool) (bool, error) { // If filtering by type, but the order type isn't known, or is something else, this entry doesn't count, move on. @@ -264,13 +268,14 @@ func (k Keeper) getPageOfOrdersFromIndex( if accumulate { // Only add it to the result if we can read it. This might result in fewer results than the limit, // but at least one bad entry won't block others by causing the whole thing to return an error. - order, err := k.parseOrderStoreValue(orderID, value) + order, err := k.getOrderFromStore(rootStore, orderID) if err == nil && order != nil { orders = append(orders, order) } } return true, nil } + prefixStore := prefix.NewStore(rootStore, prefixBz) pageResp, err := filteredPaginateAfterOrder(prefixStore, pageReq, afterOrderID, accumulator) return pageResp, orders, err @@ -326,22 +331,23 @@ func filteredPaginateAfterOrder( nextKey []byte ) + // This loop is modified from the query.FilteredPaginate version to set + // NextKey to the next hit instead of the next entry. This matches the offset behavior. for ; iterator.Valid(); iterator.Next() { - if numHits == limit { - nextKey = iterator.Key() - break - } - if iterator.Error() != nil { return nil, iterator.Error() } - hit, err := onResult(iterator.Key(), iterator.Value(), true) + hit, err := onResult(iterator.Key(), iterator.Value(), numHits < limit) if err != nil { return nil, err } if hit { + if numHits == limit { + nextKey = iterator.Key() + break + } numHits++ } } @@ -412,8 +418,7 @@ func getOrderIterator(prefixStore sdk.KVStore, start []byte, reverse bool, after // This orderIDKey is a change from the query.getIterator version. var orderIDKey []byte if afterOrderID != 0 { - // TODO[1658]: Write a unit test that hits this in order to make sure I don't need to afterOrderID++. - orderIDKey = uint64Bz(afterOrderID) + orderIDKey = uint64Bz(afterOrderID + 1) } return prefixStore.ReverseIterator(orderIDKey, end) } @@ -712,7 +717,6 @@ func (k Keeper) CancelOrder(ctx sdk.Context, orderID uint64, signer string) erro return fmt.Errorf("account %s does not have permission to cancel order %d", signer, orderID) } - signerAddr := sdk.MustAccAddressFromBech32(signer) orderOwnerAddr := sdk.MustAccAddressFromBech32(orderOwner) heldAmount := order.GetHoldAmount() err = k.holdKeeper.ReleaseHold(ctx, orderOwnerAddr, heldAmount) @@ -721,12 +725,13 @@ func (k Keeper) CancelOrder(ctx sdk.Context, orderID uint64, signer string) erro } deleteAndDeIndexOrder(k.getStore(ctx), *order) - k.emitEvent(ctx, exchange.NewEventOrderCancelled(order, signerAddr)) + k.emitEvent(ctx, exchange.NewEventOrderCancelled(order, signer)) return nil } // SetOrderExternalID updates an order's external id. +// The caller is responsible for making sure this update should be allowed (e.g. by calling CanSetIDs first). func (k Keeper) SetOrderExternalID(ctx sdk.Context, marketID uint32, orderID uint64, newExternalID string) error { if err := exchange.ValidateExternalID(newExternalID); err != nil { return err diff --git a/x/exchange/keeper/orders_test.go b/x/exchange/keeper/orders_test.go index 9e07d297e8..a4b832b005 100644 --- a/x/exchange/keeper/orders_test.go +++ b/x/exchange/keeper/orders_test.go @@ -1,21 +1,2954 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestKeeper_GetOrder() +import ( + "bytes" + "fmt" + "strings" -// TODO[1658]: func (s *TestSuite) TestKeeper_GetOrderByExternalID() + sdk "github.com/cosmos/cosmos-sdk/types" -// TODO[1658]: func (s *TestSuite) TestKeeper_CreateAskOrder() + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) -// TODO[1658]: func (s *TestSuite) TestKeeper_CreateBidOrder() +func (s *TestSuite) TestKeeper_GetOrder() { + tests := []struct { + name string + setup func() + orderID uint64 + expOrder *exchange.Order + expErr string + }{ + { + name: "empty state", + orderID: 1, + expOrder: nil, + expErr: "", + }, + { + name: "order does not exist", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: sdk.AccAddress("some_buyer__________").String(), + Assets: s.coin("20apple"), + Price: s.coin("160prune"), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(3).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: sdk.AccAddress("some_seller_________").String(), + Assets: s.coin("10apple"), + Price: s.coin("80prune"), + })) + }, + orderID: 2, + expOrder: nil, + expErr: "", + }, + { + name: "unknown type byte", + setup: func() { + order := exchange.NewOrder(5).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: sdk.AccAddress("the_seller__________").String(), + Assets: s.coin("10apple"), + Price: s.coin("2pineapple"), + AllowPartial: true, + ExternalId: "justsomeid", + }) + key, value, err := s.k.GetOrderStoreKeyValue(*order) + s.Require().NoError(err, "GetOrderStoreKeyValue") + value[0] = 99 + s.getStore().Set(key, value) + }, + orderID: 5, + expErr: "failed to read order 5: unknown type byte 0x63", + }, + { + name: "cannot read ask order", + setup: func() { + order := exchange.NewOrder(4).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: sdk.AccAddress("the_seller__________").String(), + Assets: s.coin("10apple"), + Price: s.coin("2pineapple"), + AllowPartial: true, + ExternalId: "justsomeid", + }) + key, value, err := s.k.GetOrderStoreKeyValue(*order) + s.Require().NoError(err, "GetOrderStoreKeyValue") + newValue := bytes.Repeat([]byte{3}, len(value)) + newValue[0] = value[0] + s.getStore().Set(key, newValue) + }, + orderID: 4, + expOrder: nil, + expErr: "failed to read order 4: failed to unmarshal ask order: proto: AskOrder: illegal tag 0 (wire type 3)", + }, + { + name: "cannot read bid order", + setup: func() { + order := exchange.NewOrder(4).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: sdk.AccAddress("the_buyer___________").String(), + Assets: s.coin("10apple"), + Price: s.coin("2pineapple"), + AllowPartial: true, + ExternalId: "justsomeid", + }) + key, value, err := s.k.GetOrderStoreKeyValue(*order) + s.Require().NoError(err, "GetOrderStoreKeyValue") + newValue := bytes.Repeat([]byte{3}, len(value)) + newValue[0] = value[0] + s.getStore().Set(key, newValue) + }, + orderID: 4, + expOrder: nil, + expErr: "failed to read order 4: failed to unmarshal bid order: proto: BidOrder: illegal tag 0 (wire type 3)", + }, + { + name: "order 1 ask", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 23, + Seller: sdk.AccAddress("seller_for_order_one").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + SellerSettlementFlatFee: s.coinP("10fig"), + AllowPartial: true, + ExternalId: "externalidfororder1", + })) + }, + orderID: 1, + expOrder: exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 23, + Seller: sdk.AccAddress("seller_for_order_one").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + SellerSettlementFlatFee: s.coinP("10fig"), + AllowPartial: true, + ExternalId: "externalidfororder1", + }), + }, + { + name: "order 1 bid", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 23, + Buyer: sdk.AccAddress("buyer_for_order_one_").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + BuyerSettlementFees: s.coins("10fig"), + AllowPartial: true, + ExternalId: "externalidfororder1", + })) + }, + orderID: 1, + expOrder: exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 23, + Buyer: sdk.AccAddress("buyer_for_order_one_").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + BuyerSettlementFees: s.coins("10fig"), + AllowPartial: true, + ExternalId: "externalidfororder1", + }), + }, + { + name: "order max uint32+1 ask", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_295).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("another_buyer_______").String(), + Assets: s.coin("5apple"), + Price: s.coin("5peach"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_296).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("another_seller______").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_297).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("yet_another_buyer___").String(), + Assets: s.coin("7apple"), + Price: s.coin("7peach"), + })) + }, + orderID: 4_294_967_296, + expOrder: exchange.NewOrder(4_294_967_296).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("another_seller______").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + }), + }, + { + name: "order max uint32+1 bid", + setup: func() { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_295).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("another_seller______").String(), + Assets: s.coin("5apple"), + Price: s.coin("5peach"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_296).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("another_buyer_______").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4_294_967_297).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("yet_another_seller__").String(), + Assets: s.coin("7apple"), + Price: s.coin("7peach"), + })) + }, + orderID: 4_294_967_296, + expOrder: exchange.NewOrder(4_294_967_296).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("another_buyer_______").String(), + Assets: s.coin("2apple"), + Price: s.coin("53peach"), + }), + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_CancelOrder() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_SetOrderExternalID() + var order *exchange.Order + var err error + testFunc := func() { + order, err = s.k.GetOrder(s.ctx, tc.orderID) + } + s.Require().NotPanics(testFunc, "GetOrder(%d)", tc.orderID) + s.assertErrorValue(err, tc.expErr, "GetOrder(%d) error", tc.orderID) + s.Assert().Equal(tc.expOrder, order, "GetOrder(%d) order", tc.orderID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateOrders() +func (s *TestSuite) TestKeeper_GetOrderByExternalID() { + tests := []struct { + name string + setup func() + marketID uint32 + externalID string + expOrder *exchange.Order + expErr string + }{ + { + name: "market 0", + marketID: 0, + externalID: "something", + expErr: "invalid market id: cannot be zero", + }, + { + name: "empty externalID", + marketID: 1, + externalID: "", + expOrder: nil, + expErr: "", + }, + { + name: "externalID too long", + setup: nil, + marketID: 1, + externalID: strings.Repeat("u", exchange.MaxExternalIDLength+1), + expOrder: nil, + expErr: "", + }, + { + name: "unknown externalID", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "seven", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "eight", + })) + }, + marketID: 1, + externalID: "nine", + expOrder: nil, + expErr: "", + }, + { + name: "two orders in market: first", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "seven", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "eight", + })) + }, + marketID: 3, + externalID: "seven", + expOrder: exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "seven", + }), + }, + { + name: "two orders in market: second", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "seven", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "eight", + })) + }, + marketID: 3, + externalID: "eight", + expOrder: exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "eight", + }), + }, + { + name: "externalID in two markets: first", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 55, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "specialorder", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "specialorder", + })) + }, + marketID: 55, + externalID: "specialorder", + expOrder: exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 55, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "specialorder", + }), + }, + { + name: "externalID in two markets: second", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 55, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "specialorder", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "specialorder", + })) + }, + marketID: 5, + externalID: "specialorder", + expOrder: exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "specialorder", + }), + }, + { + name: "externalID in two markets: neither", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(7).WithAsk(&exchange.AskOrder{ + MarketId: 55, + Seller: sdk.AccAddress("seller______________").String(), + Assets: s.coin("4apple"), + Price: s.coin("2plum"), + ExternalId: "specialorder", + })) + s.requireSetOrderInStore(store, exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: s.coin("5apple"), + Price: s.coin("3plum"), + ExternalId: "specialorder", + })) + }, + marketID: 15, + externalID: "specialorder", + expOrder: nil, + expErr: "", + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateMarketOrders() + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateAddressOrders() + var order *exchange.Order + var err error + testFunc := func() { + order, err = s.k.GetOrderByExternalID(s.ctx, tc.marketID, tc.externalID) + } + s.Require().NotPanics(testFunc, "GetOrderByExternalID(%d, %q)", tc.marketID, tc.externalID) + s.assertErrorValue(err, tc.expErr, "GetOrderByExternalID(%d, %q) error", tc.marketID, tc.externalID) + s.Assert().Equal(tc.expOrder, order, "GetOrderByExternalID(%d, %q) order", tc.marketID, tc.externalID) + }) + } +} -// TODO[1658]: func (s *TestSuite) TestKeeper_IterateAssetOrders() +func (s *TestSuite) TestKeeper_CreateAskOrder() { + reason := func(orderID uint64) string { + return fmt.Sprintf("x/exchange: order %d", orderID) + } + + tests := []struct { + name string + attrKeeper *MockAttributeKeeper + bankKeeper *MockBankKeeper + holdKeeper *MockHoldKeeper + setup func() + askOrder exchange.AskOrder + creationFee *sdk.Coin + expOrderID uint64 + expErr string + expBankCalls BankCalls + expHoldCalls HoldCalls + }{ + // Tests that result in errors. + { + name: "invalid order", + askOrder: exchange.AskOrder{ + MarketId: 0, + Seller: s.addr1.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "invalid market id: must not be zero", + }, + { + name: "market does not exist", + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr2.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "market 2 does not exist", + }, + { + name: "market not accepting orders", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: false, + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr3.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "market 2 is not accepting orders", + }, + { + name: "attrs required: does not have", + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(s.addr4, []string{"ccc.bb.aa"}, ""), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 7, + AcceptingOrders: true, + ReqAttrCreateAsk: []string{"cc.bb.aa"}, + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 7, + Seller: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "account " + s.addr4.String() + " is not allowed to create ask orders in market 7", + }, + { + name: "creation fee required: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + FeeCreateAskFlat: s.coins("3fig"), + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + creationFee: s.coinP("2fig"), + expErr: "insufficient ask order creation fee: \"2fig\" is less than required amount \"3fig\"", + }, + { + name: "settlement fee required: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 9, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("6fig"), + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 9, + Seller: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + SellerSettlementFlatFee: s.coinP("4fig"), + }, + expErr: "insufficient seller settlement flat fee: \"4fig\" is less than required amount \"6fig\"", + }, + { + name: "settlement fee denom same as price: price too low", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 77, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("20peach"), + FeeSellerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("100peach"), Fee: s.coin("51peach")}}, + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 77, + Seller: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("41peach"), + SellerSettlementFlatFee: s.coinP("20peach"), + }, + expErr: "price 41peach is not more than total required seller settlement fee 41peach = 20peach flat + 21peach ratio", + }, + { + name: "cannot collect creation fee", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("oh no, an error"), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 1, + Seller: s.addr5.String(), + Assets: s.coin("35apple"), + Price: s.coin("700peach"), + }, + creationFee: s.coinP("3fig"), + expErr: "error collecting ask order creation fee: error transferring 3fig from " + + s.addr5.String() + " to market 1: oh no, an error", + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{{fromAddr: s.addr5, toAddr: s.marketAddr1, amt: s.coins("3fig")}}, + }, + }, + { + name: "external id already in use in this market", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(15).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr3.String(), + Assets: s.coin("100acorn"), + Price: s.coin("30plum"), + ExternalId: "not-that-random-external-id", + })) + keeper.SetLastOrderID(store, 33) + }, + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr4.String(), + Assets: s.coin("500acorn"), + Price: s.coin("45plum"), + ExternalId: "not-that-random-external-id", + }, + expErr: "error storing ask order: external id \"not-that-random-external-id\" is " + + "already in use by order 15: cannot be used for order 34", + }, + { + name: "settlement fee denom same as price: cannot place hold on assets", + holdKeeper: NewMockHoldKeeper().WithAddHoldResults("nope, this is a test error, sorry"), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("20peach"), + }) + }, + askOrder: exchange.AskOrder{ + MarketId: 3, + Seller: s.addr1.String(), + Assets: s.coin("22apple"), + Price: s.coin("500peach"), + SellerSettlementFlatFee: s.coinP("20peach"), + }, + expErr: "error placing hold for ask order 1: nope, this is a test error, sorry", + expBankCalls: BankCalls{}, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{ + addr: s.addr1, + funds: s.coins("22apple"), + reason: "x/exchange: order 1", + }}, + }, + }, + { + name: "settlement fee denom diff from price: cannot place hold on assets and fee", + holdKeeper: NewMockHoldKeeper().WithAddHoldResults("nope, this is a test error, sorry"), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("20peach,3fig"), + }) + keeper.SetLastOrderID(s.getStore(), 5) + }, + askOrder: exchange.AskOrder{ + MarketId: 3, + Seller: s.addr1.String(), + Assets: s.coin("22apple"), + Price: s.coin("500peach"), + SellerSettlementFlatFee: s.coinP("3fig"), + }, + expErr: "error placing hold for ask order 6: nope, this is a test error, sorry", + expBankCalls: BankCalls{}, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{ + addr: s.addr1, + funds: s.coins("22apple,3fig"), + reason: reason(6), + }}, + }, + }, + + // Tests that should not give an error. + { + name: "no attrs required", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeCreateAskFlat: s.coins("10fig"), + }) + keeper.SetLastOrderID(s.getStore(), 50_000) + }, + askOrder: exchange.AskOrder{ + MarketId: 3, + Seller: s.addr3.String(), + Assets: s.coin("100apple"), + Price: s.coin("3pineapple"), + }, + creationFee: s.coinP("12fig"), + expOrderID: 50_001, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {fromAddr: s.addr3, toAddr: s.marketAddr3, amt: s.coins("12fig")}, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("1fig")}, + }, + }, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{addr: s.addr3, funds: s.coins("100apple"), reason: reason(50_001)}}, + }, + }, + { + name: "attrs required: has", + attrKeeper: NewMockAttributeKeeper().WithGetAllAttributesAddrResult(s.addr2, []string{"dd.cc.bb.aa"}, ""), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 200, + DenomSplits: []exchange.DenomSplit{{Denom: "fig", Split: 5000}}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeCreateAskFlat: s.coins("20fig"), + ReqAttrCreateAsk: []string{"*.bb.aa"}, + }) + keeper.SetLastOrderID(s.getStore(), 888) + }, + askOrder: exchange.AskOrder{ + MarketId: 3, + Seller: s.addr2.String(), + Assets: s.coin("57apple"), + Price: s.coin("3pineapple"), + }, + creationFee: s.coinP("20fig"), + expOrderID: 889, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {fromAddr: s.addr2, toAddr: s.marketAddr3, amt: s.coins("20fig")}, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("10fig")}, + }, + }, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{addr: s.addr2, funds: s.coins("57apple"), reason: reason(889)}}, + }, + }, + { + name: "no fees required", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + keeper.SetLastOrderID(s.getStore(), 1) + }, + askOrder: exchange.AskOrder{ + MarketId: 1, + Seller: s.addr4.String(), + Assets: s.coin("33apple"), + Price: s.coin("57plum"), + }, + expOrderID: 2, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr4, funds: s.coins("33apple"), reason: reason(2)}}}, + }, + { + name: "settlement fee denom same as price: hold okay", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("20peach"), + }) + keeper.SetLastOrderID(s.getStore(), 122) + }, + askOrder: exchange.AskOrder{ + MarketId: 1, + Seller: s.addr3.String(), + Assets: s.coin("500acorn"), + Price: s.coin("100peach"), + SellerSettlementFlatFee: s.coinP("20peach"), + }, + expOrderID: 123, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr3, funds: s.coins("500acorn"), reason: reason(123)}}}, + }, + { + name: "settlement fee denom diff from price: hold okay", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + FeeSellerSettlementFlat: s.coins("20peach"), + }) + keeper.SetLastOrderID(s.getStore(), 999) + }, + askOrder: exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("500acorn"), + Price: s.coin("100papaya"), + SellerSettlementFlatFee: s.coinP("20peach"), + }, + expOrderID: 1000, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("500acorn,20peach"), reason: reason(1000)}}}, + }, + { + name: "external id in use but in different market", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "unoriginal", + })) + keeper.SetLastOrderID(store, 98765) + }, + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "unoriginal", + }, + expOrderID: 98766, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("11acorn"), reason: reason(98766)}}}, + }, + { + name: "new external id", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "C52B5350-BBD6-48B4-9AA7-2F2197260F9D", + })) + keeper.SetLastOrderID(store, 65) + }, + askOrder: exchange.AskOrder{ + MarketId: 2, + Seller: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "C52B5350-BBD6-48B4-9AA7-2F2197260F9E", + }, + expOrderID: 66, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("11acorn"), reason: reason(66)}}}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expOrder *exchange.Order + var expEvents sdk.Events + if len(tc.expErr) == 0 { + expOrder = exchange.NewOrder(tc.expOrderID).WithAsk(&tc.askOrder) + event := exchange.NewEventOrderCreated(expOrder) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + kpr := s.k.WithAttributeKeeper(tc.attrKeeper).WithBankKeeper(tc.bankKeeper).WithHoldKeeper(tc.holdKeeper) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var orderID uint64 + var err error + testFunc := func() { + orderID, err = kpr.CreateAskOrder(ctx, tc.askOrder, tc.creationFee) + } + s.Require().NotPanics(testFunc, "CreateAskOrder") + s.assertErrorValue(err, tc.expErr, "CreateAskOrder error") + s.assertEqualOrderID(tc.expOrderID, orderID, "CreateAskOrder order id") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "CreateAskOrder events") + s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "CreateAskOrder") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "CreateAskOrder") + + if len(tc.expErr) > 0 || err != nil { + return + } + + order, err := s.k.GetOrder(s.ctx, orderID) + s.Require().NoError(err, "GetOrder(%d) error (the one just created)", orderID) + s.Assert().Equal(expOrder, order, "GetOrder(%d) (the one just created)", orderID) + lastOrderID := keeper.GetLastOrderID(s.getStore()) + s.assertEqualOrderID(tc.expOrderID, lastOrderID, "last order id") + }) + } +} + +func (s *TestSuite) TestKeeper_CreateBidOrder() { + reason := func(orderID uint64) string { + return fmt.Sprintf("x/exchange: order %d", orderID) + } + + tests := []struct { + name string + attrKeeper *MockAttributeKeeper + bankKeeper *MockBankKeeper + holdKeeper *MockHoldKeeper + setup func() + bidOrder exchange.BidOrder + creationFee *sdk.Coin + expOrderID uint64 + expErr string + expBankCalls BankCalls + expHoldCalls HoldCalls + }{ + // Tests that result in errors. + { + name: "invalid order", + bidOrder: exchange.BidOrder{ + MarketId: 0, + Buyer: s.addr1.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "invalid market id: must not be zero", + }, + { + name: "market does not exist", + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "market 2 does not exist", + }, + { + name: "market not accepting orders", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: false, + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr3.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "market 2 is not accepting orders", + }, + { + name: "attrs required: does not have", + attrKeeper: NewMockAttributeKeeper(). + WithGetAllAttributesAddrResult(s.addr4, []string{"ccc.bb.aa"}, ""), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 7, + AcceptingOrders: true, + ReqAttrCreateBid: []string{"cc.bb.aa"}, + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 7, + Buyer: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + expErr: "account " + s.addr4.String() + " is not allowed to create bid orders in market 7", + }, + { + name: "creation fee required: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + FeeCreateBidFlat: s.coins("3fig"), + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr4.String(), + Assets: s.coin("35apple"), + Price: s.coin("10peach"), + }, + creationFee: s.coinP("2fig"), + expErr: "insufficient bid order creation fee: \"2fig\" is less than required amount \"3fig\"", + }, + { + name: "only buyer flat: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + FeeBuyerSettlementFlat: s.coins("10fig"), + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("100acorn"), + Price: s.coin("77plum"), + BuyerSettlementFees: s.coins("9fig"), + }, + expErr: s.joinErrs( + "9fig is less than required flat fee 10fig", + "required flat fee not satisfied, valid options: 10fig", + "insufficient buyer settlement fee 9fig", + ), + }, + { + name: "only buyer ratio: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + FeeBuyerSettlementRatios: s.ratios("100plum:3plum"), + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("100acorn"), + Price: s.coin("400plum"), + BuyerSettlementFees: s.coins("11plum"), + }, + expErr: s.joinErrs( + "11plum is less than required ratio fee 12plum (based on price 400plum and ratio 100plum:3plum)", + "required ratio fee not satisfied, valid ratios: 100plum:3plum", + "insufficient buyer settlement fee 11plum", + ), + }, + { + name: "buyer flat and ratio: not enough", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + FeeBuyerSettlementFlat: s.coins("10plum"), + FeeBuyerSettlementRatios: s.ratios("100plum:3plum"), + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("100acorn"), + Price: s.coin("400plum"), + BuyerSettlementFees: s.coins("21plum"), + }, + expErr: s.joinErrs( + "21plum is less than combined fee 22plum = 10plum (flat) + 12plum (ratio based on price 400plum)", + "insufficient buyer settlement fee 21plum", + ), + }, + { + name: "cannot collect creation fee", + bankKeeper: NewMockBankKeeper().WithSendCoinsResults("oh no, an error"), + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + }, + bidOrder: exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr5.String(), + Assets: s.coin("35apple"), + Price: s.coin("700peach"), + }, + creationFee: s.coinP("3fig"), + expErr: "error collecting bid order creation fee: error transferring 3fig from " + + s.addr5.String() + " to market 1: oh no, an error", + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{{fromAddr: s.addr5, toAddr: s.marketAddr1, amt: s.coins("3fig")}}, + }, + }, + { + name: "external id already in use in this market", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(15).WithAsk(&exchange.AskOrder{ + MarketId: 2, + Seller: s.addr3.String(), + Assets: s.coin("100acorn"), + Price: s.coin("30plum"), + ExternalId: "not-that-random-external-id", + })) + keeper.SetLastOrderID(store, 33) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr4.String(), + Assets: s.coin("500acorn"), + Price: s.coin("45plum"), + ExternalId: "not-that-random-external-id", + }, + expErr: "error storing bid order: external id \"not-that-random-external-id\" is " + + "already in use by order 15: cannot be used for order 34", + }, + { + name: "no settlement fee: cannot place hold", + holdKeeper: NewMockHoldKeeper().WithAddHoldResults("injected problem"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 0}) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + }) + keeper.SetLastOrderID(s.getStore(), 776) + }, + bidOrder: exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr1.String(), + Assets: s.coin("11apple"), + Price: s.coin("55peach"), + }, + creationFee: s.coinP("3fig"), + expErr: "error placing hold for bid order 777: injected problem", + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{{fromAddr: s.addr1, toAddr: s.marketAddr3, amt: s.coins("3fig")}}}, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("55peach"), reason: reason(777)}}}, + }, + { + name: "with settlement fee: cannot place hold", + holdKeeper: NewMockHoldKeeper().WithAddHoldResults("injected problem"), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{DefaultSplit: 0}) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + }) + keeper.SetLastOrderID(s.getStore(), 83483) + }, + bidOrder: exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr1.String(), + Assets: s.coin("11apple"), + Price: s.coin("55peach"), + BuyerSettlementFees: s.coins("2peach,5grape"), + }, + creationFee: s.coinP("3fig"), + expErr: "error placing hold for bid order 83484: injected problem", + expBankCalls: BankCalls{SendCoins: []*SendCoinsArgs{{fromAddr: s.addr1, toAddr: s.marketAddr3, amt: s.coins("3fig")}}}, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("5grape,57peach"), reason: reason(83484)}}}, + }, + + // Tests that should not give an error. + { + name: "no attrs required", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeCreateBidFlat: s.coins("10fig"), + }) + keeper.SetLastOrderID(s.getStore(), 50_000) + }, + bidOrder: exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr3.String(), + Assets: s.coin("100apple"), + Price: s.coin("3pineapple"), + }, + creationFee: s.coinP("12fig"), + expOrderID: 50_001, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {fromAddr: s.addr3, toAddr: s.marketAddr3, amt: s.coins("12fig")}, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("1fig")}, + }, + }, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{addr: s.addr3, funds: s.coins("3pineapple"), reason: reason(50_001)}}, + }, + }, + { + name: "attrs required: has", + attrKeeper: NewMockAttributeKeeper().WithGetAllAttributesAddrResult(s.addr2, []string{"dd.cc.bb.aa"}, ""), + setup: func() { + s.k.SetParams(s.ctx, &exchange.Params{ + DefaultSplit: 200, + DenomSplits: []exchange.DenomSplit{{Denom: "fig", Split: 5000}}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + FeeCreateBidFlat: s.coins("20fig"), + ReqAttrCreateBid: []string{"*.bb.aa"}, + }) + keeper.SetLastOrderID(s.getStore(), 888) + }, + bidOrder: exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr2.String(), + Assets: s.coin("57apple"), + Price: s.coin("3pineapple"), + }, + creationFee: s.coinP("20fig"), + expOrderID: 889, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {fromAddr: s.addr2, toAddr: s.marketAddr3, amt: s.coins("20fig")}, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr3, recipientModule: s.feeCollector, amt: s.coins("10fig")}, + }, + }, + expHoldCalls: HoldCalls{ + AddHold: []*AddHoldArgs{{addr: s.addr2, funds: s.coins("3pineapple"), reason: reason(889)}}, + }, + }, + { + name: "no fees required", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + keeper.SetLastOrderID(s.getStore(), 1) + }, + bidOrder: exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr4.String(), + Assets: s.coin("33apple"), + Price: s.coin("57plum"), + }, + expOrderID: 2, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr4, funds: s.coins("57plum"), reason: reason(2)}}}, + }, + { + name: "no settlement fee: hold okay", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + FeeCreateBidFlat: s.coins("5fig"), + }) + keeper.SetLastOrderID(s.getStore(), 122) + }, + bidOrder: exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr3.String(), + Assets: s.coin("500acorn"), + Price: s.coin("100peach"), + }, + creationFee: s.coinP("5fig"), + expOrderID: 123, + expBankCalls: BankCalls{ + SendCoins: []*SendCoinsArgs{ + {fromAddr: s.addr3, toAddr: s.marketAddr1, amt: s.coins("5fig")}, + }, + SendCoinsFromAccountToModule: []*SendCoinsFromAccountToModuleArgs{ + {senderAddr: s.marketAddr1, recipientModule: s.feeCollector, amt: s.coins("1fig")}, + }, + }, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr3, funds: s.coins("100peach"), reason: reason(123)}}}, + }, + { + name: "with settlement fee: hold okay", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + FeeBuyerSettlementFlat: s.coins("20fig"), + FeeBuyerSettlementRatios: []exchange.FeeRatio{{Price: s.coin("100peach"), Fee: s.coin("3peach")}}, + }) + keeper.SetLastOrderID(s.getStore(), 999) + }, + bidOrder: exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("500acorn"), + Price: s.coin("1000peach"), + BuyerSettlementFees: s.coins("20fig,30peach"), + }, + expOrderID: 1000, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("20fig,1030peach"), reason: reason(1000)}}}, + }, + { + name: "external id in use but in different market", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "unoriginal", + })) + keeper.SetLastOrderID(store, 98765) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "unoriginal", + }, + expOrderID: 98766, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("55plum"), reason: reason(98766)}}}, + }, + { + name: "new external id", + setup: func() { + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + }) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(55).WithAsk(&exchange.AskOrder{ + MarketId: 2, + Seller: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "C52B5350-BBD6-48B4-9AA7-2F2197260F9D", + })) + keeper.SetLastOrderID(store, 65) + }, + bidOrder: exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr1.String(), + Assets: s.coin("11acorn"), + Price: s.coin("55plum"), + ExternalId: "C52B5350-BBD6-48B4-9AA7-2F2197260F9E", + }, + expOrderID: 66, + expHoldCalls: HoldCalls{AddHold: []*AddHoldArgs{{addr: s.addr1, funds: s.coins("55plum"), reason: reason(66)}}}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + var expOrder *exchange.Order + var expEvents sdk.Events + if len(tc.expErr) == 0 { + expOrder = exchange.NewOrder(tc.expOrderID).WithBid(&tc.bidOrder) + event := exchange.NewEventOrderCreated(expOrder) + expEvents = append(expEvents, s.untypeEvent(event)) + } + + if tc.attrKeeper == nil { + tc.attrKeeper = NewMockAttributeKeeper() + } + if tc.bankKeeper == nil { + tc.bankKeeper = NewMockBankKeeper() + } + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + kpr := s.k.WithAttributeKeeper(tc.attrKeeper).WithBankKeeper(tc.bankKeeper).WithHoldKeeper(tc.holdKeeper) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var orderID uint64 + var err error + testFunc := func() { + orderID, err = kpr.CreateBidOrder(ctx, tc.bidOrder, tc.creationFee) + } + s.Require().NotPanics(testFunc, "CreateBidOrder") + s.assertErrorValue(err, tc.expErr, "CreateBidOrder error") + s.assertEqualOrderID(tc.expOrderID, orderID, "CreateBidOrder order id") + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "CreateBidOrder events") + s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "CreateBidOrder") + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "CreateBidOrder") + + if len(tc.expErr) > 0 || err != nil { + return + } + + order, err := s.k.GetOrder(s.ctx, orderID) + s.Require().NoError(err, "error from GetOrder(%d) (the one just created)", orderID) + s.Assert().Equal(expOrder, order, "GetOrder(%d) (the one just created)", orderID) + lastOrderID := keeper.GetLastOrderID(s.getStore()) + s.assertEqualOrderID(tc.expOrderID, lastOrderID, "last order id") + }) + } +} + +func (s *TestSuite) TestKeeper_CancelOrder() { + tests := []struct { + name string + holdKeeper *MockHoldKeeper + setup func() *exchange.Order // should return the order expected to be cancelled. + orderID uint64 + signer string + expErr string + expHoldCalls HoldCalls + }{ + { + name: "error getting order", + setup: func() *exchange.Order { + order := exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr3.String(), + Assets: s.coin("50apricot"), + Price: s.coin("333prune"), + }) + key, value, err := s.k.GetOrderStoreKeyValue(*order) + s.Require().NoError(err, "GetOrderStoreKeyValue") + value[0] = 9 + s.getStore().Set(key, value) + return nil + }, + orderID: 3, + signer: s.addr3.String(), + expErr: "failed to read order 3: unknown type byte 0x9", + }, + { + name: "order does not exist", + orderID: 55, + signer: s.addr1.String(), + expErr: "order 55 does not exist", + }, + { + name: "signer not allowed", + setup: func() *exchange.Order { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(8).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr3.String(), + Assets: s.coin("50apricot"), + Price: s.coin("333prune"), + })) + return nil + }, + orderID: 8, + signer: s.addr2.String(), + expErr: "account " + s.addr2.String() + " does not have permission to cancel order 8", + }, + { + name: "error releasing hold", + holdKeeper: NewMockHoldKeeper().WithReleaseHoldResults("there's not enough here"), + setup: func() *exchange.Order { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(7).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr3.String(), + Assets: s.coin("50apricot"), + Price: s.coin("333prune"), + })) + return nil + }, + orderID: 7, + signer: s.addr3.String(), + expErr: "unable to release hold on order 7 funds: there's not enough here", + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr3, funds: s.coins("333prune")}}}, + }, + { + name: "signer can cancel in other market but not this one", + setup: func() *exchange.Order { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_cancel)}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 2, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{s.agCanAllBut(s.addr5, exchange.Permission_cancel)}, + }) + s.requireCreateMarket(exchange.Market{ + MarketId: 3, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr5, exchange.Permission_cancel)}, + }) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr3.String(), + Assets: s.coin("50apricot"), + Price: s.coin("333prune"), + })) + return nil + }, + orderID: 2, + signer: s.addr5.String(), + expErr: "account " + s.addr5.String() + " does not have permission to cancel order 2", + }, + { + name: "signer is ask order seller", + setup: func() *exchange.Order { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(51).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("50apricot"), + Price: s.coin("55plum"), + ExternalId: "order 51", + })) + orderToCancel := exchange.NewOrder(52).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("50apricot"), + Price: s.coin("55plum"), + SellerSettlementFlatFee: s.coinP("8fig"), + ExternalId: "bananas", + }) + s.requireSetOrderInStore(store, orderToCancel) + s.requireSetOrderInStore(store, exchange.NewOrder(53).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr1.String(), + Assets: s.coin("6apple"), + Price: s.coin("55plum"), + ExternalId: "order 53", + })) + return orderToCancel + }, + orderID: 52, + signer: s.addr1.String(), + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr1, funds: s.coins("50apricot,8fig")}}}, + }, + { + name: "signer is bid order buyer", + setup: func() *exchange.Order { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(56).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: s.addr4.String(), + Assets: s.coin("12apple"), + Price: s.coin("55plum"), + ExternalId: "order 56", + })) + orderToCancel := exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr4.String(), + Assets: s.coin("50apricot"), + Price: s.coin("55plum"), + BuyerSettlementFees: s.coins("8fig"), + ExternalId: "whatever", + }) + s.requireSetOrderInStore(store, orderToCancel) + s.requireSetOrderInStore(store, exchange.NewOrder(58).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: s.addr4.String(), + Assets: s.coin("13apple"), + Price: s.coin("80plum"), + ExternalId: "order 58", + })) + return orderToCancel + }, + orderID: 57, + signer: s.addr4.String(), + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr4, funds: s.coins("8fig,55plum")}}}, + }, + { + name: "signer is authority", + setup: func() *exchange.Order { + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(99).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("12apple"), + Price: s.coin("55plum"), + ExternalId: "order 99", + })) + orderToCancel := exchange.NewOrder(100).WithAsk(&exchange.AskOrder{ + MarketId: 2, + Seller: s.addr3.String(), + Assets: s.coin("12apricot"), + Price: s.coin("73pear"), + SellerSettlementFlatFee: s.coinP("1pear"), + ExternalId: "whatever", + }) + s.requireSetOrderInStore(store, orderToCancel) + s.requireSetOrderInStore(store, exchange.NewOrder(101).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: s.addr5.String(), + Assets: s.coin("13apple"), + Price: s.coin("80plum"), + ExternalId: "order 101", + })) + return orderToCancel + }, + orderID: 100, + signer: s.k.GetAuthority(), + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr3, funds: s.coins("12apricot")}}}, + }, + { + name: "signer can cancel in market", + setup: func() *exchange.Order { + s.requireCreateMarket(exchange.Market{ + MarketId: 1, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{s.agCanOnly(s.addr1, exchange.Permission_cancel)}, + }) + orderToCancel := exchange.NewOrder(999).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("12apple"), + Price: s.coin("55plum"), + ExternalId: "order 999", + }) + s.requireSetOrderInStore(s.getStore(), orderToCancel) + return orderToCancel + }, + orderID: 999, + signer: s.addr1.String(), + expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("55plum")}}}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + var cancelledOrder *exchange.Order + if tc.setup != nil { + cancelledOrder = tc.setup() + } + + var expEvents sdk.Events + var expDelKVs []sdk.KVPair + if cancelledOrder != nil { + event := exchange.NewEventOrderCancelled(cancelledOrder, tc.signer) + expEvents = append(expEvents, s.untypeEvent(event)) + + expDelKVs = keeper.CreateConstantIndexEntries(*cancelledOrder) + extIDKV := keeper.CreateMarketExternalIDToOrderEntry(cancelledOrder) + if extIDKV != nil { + expDelKVs = append(expDelKVs, *extIDKV) + } + } + + if tc.holdKeeper == nil { + tc.holdKeeper = NewMockHoldKeeper() + } + kpr := s.k.WithHoldKeeper(tc.holdKeeper) + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = kpr.CancelOrder(ctx, tc.orderID, tc.signer) + } + s.Require().NotPanics(testFunc, "CancelOrder(%d, %q)", tc.orderID, tc.signer) + s.assertErrorValue(err, tc.expErr, "CancelOrder(%d, %q) error", tc.orderID, tc.signer) + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "CancelOrder(%d, %q) events", tc.orderID, tc.signer) + s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "CancelOrder(%d, %q)", tc.orderID, tc.signer) + + if err != nil || len(tc.expErr) > 0 { + return + } + + order, err := s.k.GetOrder(s.ctx, tc.orderID) + s.Assert().NoError(err, "GetOrder(%d) error after cancel") + s.Assert().Nil(order, "GetOrder(%d) order after cancel") + store := s.getStore() + for i, kv := range expDelKVs { + has := store.Has(kv.Key) + s.Assert().False(has, "[%d]: store.Has(%q) (index entry) after cancel", i, kv.Key) + } + }) + } +} + +func (s *TestSuite) TestKeeper_SetOrderExternalID() { + tests := []struct { + name string + setup func() string // should return the original externalID + marketID uint32 + orderID uint64 + newExternalID string + expErr string + }{ + { + name: "new external id too long", + marketID: 1, + orderID: 1, + newExternalID: strings.Repeat("I", exchange.MaxExternalIDLength+1), + expErr: fmt.Sprintf("invalid external id %q: max length %d", + strings.Repeat("I", exchange.MaxExternalIDLength+1), exchange.MaxExternalIDLength), + }, + { + name: "error getting order", + setup: func() string { + key, value, err := s.k.GetOrderStoreKeyValue(*exchange.NewOrder(5).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr1.String(), + Assets: s.coin("1apple"), + Price: s.coin("1pear"), + })) + s.Require().NoError(err, "GetOrderStoreKeyValue") + value[0] = 9 + s.getStore().Set(key, value) + return "" + }, + marketID: 1, + orderID: 5, + newExternalID: "something", + expErr: "failed to read order 5: unknown type byte 0x9", + }, + { + name: "unknown order", + marketID: 1, + orderID: 1, + newExternalID: "", + expErr: "order 1 not found", + }, + { + name: "wrong market id", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr5.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + })) + return "" + }, + marketID: 2, + orderID: 3, + newExternalID: "what", + expErr: "order 3 has market id 1, expected 2", + }, + { + name: "unchanged external id", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr5.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "thisisfruity", + })) + return "" + }, + marketID: 1, + orderID: 3, + newExternalID: "thisisfruity", + expErr: "order 3 already has external id \"thisisfruity\"", + }, + { + name: "nothing to nothing", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr5.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + return "" + }, + marketID: 1, + orderID: 3, + newExternalID: "", + expErr: "order 3 already has external id \"\"", + }, + { + name: "new external id already exists in market", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(3).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr5.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "duplicate", + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(4).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr5.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + return "" + }, + marketID: 2, + orderID: 4, + newExternalID: "duplicate", + expErr: "external id \"duplicate\" is already in use by order 3: cannot be used for order 4", + }, + { + name: "nothing to something: ask", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(57).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr3.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + return "" + }, + marketID: 1, + orderID: 57, + newExternalID: "something", + }, + { + name: "nothing to something: bid", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + return "" + }, + marketID: 1, + orderID: 57, + newExternalID: "something", + }, + { + name: "something to nothing: ask", + setup: func() string { + // make sure it's okay to have multiple without an external id. + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(57).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr4.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + oldVal := "changeme" + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(58).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: oldVal, + })) + return oldVal + }, + marketID: 1, + orderID: 58, + newExternalID: "", + }, + { + name: "something to nothing: bid", + setup: func() string { + // make sure it's okay to have multiple without an external id. + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(57).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + oldVal := "changeme" + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(58).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: oldVal, + })) + return oldVal + }, + marketID: 1, + orderID: 58, + newExternalID: "", + }, + { + name: "something to something else: ask", + setup: func() string { + oldVal := "alterthis" + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(6).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: s.addr3.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: oldVal, + })) + return oldVal + }, + marketID: 1, + orderID: 6, + newExternalID: "consideritaltered", + }, + { + name: "something to something else: bid", + setup: func() string { + oldVal := "alterthis" + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(6).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: oldVal, + })) + return oldVal + }, + marketID: 1, + orderID: 6, + newExternalID: "consideritaltered", + }, + { + name: "new external id exists but in different market", + setup: func() string { + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "sharedval", + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(6).WithBid(&exchange.BidOrder{ + MarketId: 2, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "", + })) + s.requireSetOrderInStore(s.getStore(), exchange.NewOrder(7).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr2.String(), + Assets: s.coin("7acai"), + Price: s.coin("1papaya"), + ExternalId: "sharedval", + })) + return "" + }, + marketID: 2, + orderID: 6, + newExternalID: "sharedval", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + var origExternalID string + if tc.setup != nil { + origExternalID = tc.setup() + } + + var expEvents sdk.Events + var expOrder *exchange.Order + if len(tc.expErr) == 0 { + event := &exchange.EventOrderExternalIDUpdated{ + OrderId: tc.orderID, + MarketId: tc.marketID, + ExternalId: tc.newExternalID, + } + expEvents = append(expEvents, s.untypeEvent(event)) + + var err error + expOrder, err = s.k.GetOrder(s.ctx, tc.orderID) + s.Require().NoError(err, "GetOrder(%d) before anything", tc.orderID) + switch { + case expOrder.IsAskOrder(): + askOrder := expOrder.GetAskOrder() + askOrder.ExternalId = tc.newExternalID + expOrder = exchange.NewOrder(expOrder.OrderId).WithAsk(askOrder) + case expOrder.IsBidOrder(): + bidOrder := expOrder.GetBidOrder() + bidOrder.ExternalId = tc.newExternalID + expOrder = exchange.NewOrder(expOrder.OrderId).WithBid(bidOrder) + } + } + + em := sdk.NewEventManager() + ctx := s.ctx.WithEventManager(em) + var err error + testFunc := func() { + err = s.k.SetOrderExternalID(ctx, tc.marketID, tc.orderID, tc.newExternalID) + } + s.Require().NotPanics(testFunc, "SetOrderExternalID(%d, %d, %q)", tc.marketID, tc.orderID, tc.newExternalID) + s.assertErrorValue(err, tc.expErr, "SetOrderExternalID(%d, %d, %q) error", tc.marketID, tc.orderID, tc.newExternalID) + actEvents := em.Events() + s.assertEqualEvents(expEvents, actEvents, "SetOrderExternalID(%d, %d, %q) events", tc.marketID, tc.orderID, tc.newExternalID) + + if err != nil || len(tc.expErr) != 0 { + return + } + + order, err := s.k.GetOrder(s.ctx, tc.orderID) + if s.Assert().NoError(err, "GetOrder(%d) error", tc.orderID) { + s.Assert().Equal(expOrder, order, "GetOrder(%d) result (after SetOrderExternalID)", tc.orderID) + } + oldOrder, err := s.k.GetOrderByExternalID(s.ctx, tc.marketID, origExternalID) + s.Assert().NoError(err, "error from GetOrderByExternalID(%d, %q) (original ExternalID)", tc.marketID, origExternalID) + s.Assert().Nil(oldOrder, "result from GetOrderByExternalID(%d, %q) (original ExternalID)", tc.marketID, origExternalID) + }) + } +} + +func (s *TestSuite) TestKeeper_IterateOrders() { + var orders []*exchange.Order + getAll := func(order *exchange.Order) bool { + orders = append(orders, order) + return false + } + stopAfter := func(count int) func(order *exchange.Order) bool { + return func(order *exchange.Order) bool { + orders = append(orders, order) + return len(orders) >= count + } + } + addr := func(prefix string, orderID uint64) sdk.AccAddress { + return sdk.AccAddress(fmt.Sprintf("%s_%d__________________", prefix, orderID)[:20]) + } + askOrder := func(orderID uint64) *exchange.Order { + return exchange.NewOrder(orderID).WithAsk(&exchange.AskOrder{ + MarketId: uint32(orderID / 10), + Seller: addr("seller", orderID).String(), + Assets: sdk.NewInt64Coin("apple", int64(orderID)), + Price: sdk.NewInt64Coin("papaya", int64(orderID)), + AllowPartial: orderID%2 == 0, + ExternalId: fmt.Sprintf("external%d", orderID), + }) + } + bidOrder := func(orderID uint64) *exchange.Order { + return exchange.NewOrder(orderID).WithBid(&exchange.BidOrder{ + MarketId: uint32(orderID / 10), + Buyer: addr("buyer", orderID).String(), + Assets: sdk.NewInt64Coin("apple", int64(orderID)), + Price: sdk.NewInt64Coin("papaya", int64(orderID)), + AllowPartial: orderID%2 == 0, + ExternalId: fmt.Sprintf("external%d", orderID), + }) + } + + tests := []struct { + name string + setup func() + cb func(order *exchange.Order) bool + expErr string + expOrders []*exchange.Order + }{ + { + name: "empty state", + expOrders: nil, + }, + { + name: "one order: ask", + setup: func() { + s.requireSetOrderInStore(s.getStore(), askOrder(8)) + }, + expOrders: []*exchange.Order{askOrder(8)}, + }, + { + name: "one order: bid", + setup: func() { + s.requireSetOrderInStore(s.getStore(), bidOrder(8)) + }, + expOrders: []*exchange.Order{bidOrder(8)}, + }, + { + name: "one order: bad key", + setup: func() { + key, value, err := s.k.GetOrderStoreKeyValue(*askOrder(4)) + s.Require().NoError(err, "GetOrderStoreKeyValue") + s.getStore().Set(s.badKey(key), value) + }, + expErr: "invalid order store key [0 0 0 0 0 0 4]: length expected to be at least 8", + }, + { + name: "one order: bad value", + setup: func() { + key, value, err := s.k.GetOrderStoreKeyValue(*askOrder(3)) + s.Require().NoError(err, "GetOrderStoreKeyValue") + value[0] = 8 + s.getStore().Set(key, value) + }, + expErr: "failed to read order 3: unknown type byte 0x8", + }, + { + name: "five orders, 1 through 5: get all", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, askOrder(1)) + s.requireSetOrderInStore(store, bidOrder(2)) + s.requireSetOrderInStore(store, bidOrder(3)) + s.requireSetOrderInStore(store, askOrder(4)) + s.requireSetOrderInStore(store, askOrder(5)) + }, + expOrders: []*exchange.Order{ + askOrder(1), bidOrder(2), bidOrder(3), askOrder(4), askOrder(5), + }, + }, + { + name: "five orders, 1 through 5: get one", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(1)) + s.requireSetOrderInStore(store, bidOrder(2)) + s.requireSetOrderInStore(store, askOrder(3)) + s.requireSetOrderInStore(store, bidOrder(4)) + s.requireSetOrderInStore(store, askOrder(5)) + }, + cb: stopAfter(1), + expErr: "", + expOrders: []*exchange.Order{bidOrder(1)}, + }, + { + name: "five orders, 1 through 5: get three", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(1)) + s.requireSetOrderInStore(store, askOrder(2)) + s.requireSetOrderInStore(store, askOrder(3)) + s.requireSetOrderInStore(store, bidOrder(4)) + s.requireSetOrderInStore(store, askOrder(5)) + }, + cb: stopAfter(3), + expOrders: []*exchange.Order{bidOrder(1), askOrder(2), askOrder(3)}, + }, + { + name: "five orders, random: get all", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(57)) + s.requireSetOrderInStore(store, bidOrder(78)) + s.requireSetOrderInStore(store, askOrder(83)) + s.requireSetOrderInStore(store, bidOrder(47)) + s.requireSetOrderInStore(store, askOrder(28)) + }, + expOrders: []*exchange.Order{ + askOrder(28), bidOrder(47), bidOrder(57), bidOrder(78), askOrder(83), + }, + }, + { + name: "five orders, random: get one", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(57)) + s.requireSetOrderInStore(store, bidOrder(78)) + s.requireSetOrderInStore(store, askOrder(83)) + s.requireSetOrderInStore(store, bidOrder(47)) + s.requireSetOrderInStore(store, askOrder(28)) + }, + cb: stopAfter(1), + expOrders: []*exchange.Order{askOrder(28)}, + }, + { + name: "five orders, random: get three", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(57)) + s.requireSetOrderInStore(store, bidOrder(78)) + s.requireSetOrderInStore(store, askOrder(83)) + s.requireSetOrderInStore(store, bidOrder(47)) + s.requireSetOrderInStore(store, askOrder(28)) + }, + cb: stopAfter(3), + expOrders: []*exchange.Order{ + askOrder(28), bidOrder(47), bidOrder(57), + }, + }, + { + name: "three orders: second bad", + setup: func() { + store := s.getStore() + s.requireSetOrderInStore(store, bidOrder(6)) + key, value, err := s.k.GetOrderStoreKeyValue(*bidOrder(74)) + s.Require().NoError(err, "GetOrderStoreKeyValue") + value[0] = 8 + store.Set(key, value) + s.requireSetOrderInStore(store, askOrder(91)) + }, + expErr: "failed to read order 74: unknown type byte 0x8", + expOrders: []*exchange.Order{bidOrder(6), askOrder(91)}, + }, + { + name: "three orders: all bad", + setup: func() { + store := s.getStore() + + key6, value6, err := s.k.GetOrderStoreKeyValue(*askOrder(6)) + s.Require().NoError(err, "GetOrderStoreKeyValue 6") + value6[0] = 6 + store.Set(key6, value6) + + key74, value74, err := s.k.GetOrderStoreKeyValue(*bidOrder(74)) + s.Require().NoError(err, "GetOrderStoreKeyValue 74") + value74[0] = 74 + store.Set(key74, value74) + + key91, value91, err := s.k.GetOrderStoreKeyValue(*bidOrder(91)) + s.Require().NoError(err, "GetOrderStoreKeyValue 91") + value91[0] = 91 + store.Set(key91, value91) + }, + cb: stopAfter(1), // should never get there. + expErr: s.joinErrs( + "failed to read order 6: unknown type byte 0x6", + "failed to read order 74: unknown type byte 0x4a", + "failed to read order 91: unknown type byte 0x5b", + ), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.cb == nil { + tc.cb = getAll + } + + orders = nil + var err error + testFunc := func() { + err = s.k.IterateOrders(s.ctx, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateOrders") + s.assertErrorValue(err, tc.expErr, "IterateOrders error") + s.assertEqualOrders(tc.expOrders, orders, "orders iterated") + }) + } +} + +// orderIterCBArgs are the args provided to an order index iterator. +type orderIterCBArgs struct { + orderID uint64 + orderTypeByte byte +} + +// orderIDString gets this order id as a string. +func (a orderIterCBArgs) orderIDString() string { + return fmt.Sprintf("%d", a.orderID) +} + +// newOrderIterCBArgs creates a new orderIterCBArgs. +func newOrderIterCBArgs(orderID uint64, orderTypeByte byte) orderIterCBArgs { + return orderIterCBArgs{ + orderID: orderID, + orderTypeByte: orderTypeByte, + } +} + +func (s *TestSuite) TestKeeper_IterateMarketOrders() { + var seen []orderIterCBArgs + getAll := func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return false + } + stopAfter := func(count int) func(orderID uint64, orderTypeByte byte) bool { + return func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return len(seen) >= count + } + } + + tests := []struct { + name string + setup func() + marketID uint32 + cb func(orderID uint64, orderTypeByte byte) bool + expSeen []orderIterCBArgs + }{ + { + name: "empty state", + marketID: 3, + expSeen: nil, + }, + { + name: "no orders in market", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 2, + expSeen: nil, + }, + { + name: "one entry: ask", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(2, 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 2, + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk)}, + }, + { + name: "one entry: bid", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(2, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 2, + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeBid)}, + }, + { + name: "one entry no value", + setup: func() { + s.getStore().Set(keeper.MakeIndexKeyMarketToOrder(2, 4), []byte{}) + }, + marketID: 2, + expSeen: nil, + }, + { + name: "one entry bad key", + setup: func() { + s.getStore().Set(s.badKey(keeper.MakeIndexKeyMarketToOrder(2, 4)), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 2, + expSeen: nil, + }, + { + name: "five entries, 1 through 5: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 5), []byte{keeper.OrderKeyTypeBid}) + }, + marketID: 4, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(5, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, 1 through 5: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 5), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 4, + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(1, keeper.OrderKeyTypeBid)}, + }, + { + name: "five entries, 1 through 5: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(4, 5), []byte{keeper.OrderKeyTypeBid}) + }, + marketID: 4, + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk), + }, + }, + { + name: "five entries, random: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 44), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 96), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 56), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 7, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(56, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(75, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(96, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, random: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 56), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 7, + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk)}, + }, + { + name: "five entries, random: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 75), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(7, 56), []byte{keeper.OrderKeyTypeBid}) + }, + marketID: 7, + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(56, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries: two are bad", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyMarketToOrder(27, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(27, 2), []byte{}) + store.Set(s.badKey(keeper.MakeIndexKeyMarketToOrder(27, 3)), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyMarketToOrder(27, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyMarketToOrder(27, 5), []byte{keeper.OrderKeyTypeAsk}) + }, + marketID: 27, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(4, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(5, keeper.OrderKeyTypeAsk), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.cb == nil { + tc.cb = getAll + } + + seen = nil + testFunc := func() { + s.k.IterateMarketOrders(s.ctx, tc.marketID, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateMarketOrders(%d)", tc.marketID) + assertEqualSlice(s, tc.expSeen, seen, orderIterCBArgs.orderIDString, "args provided to callback") + }) + } +} + +func (s *TestSuite) TestKeeper_IterateAddressOrders() { + var seen []orderIterCBArgs + getAll := func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return false + } + stopAfter := func(count int) func(orderID uint64, orderTypeByte byte) bool { + return func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return len(seen) >= count + } + } + + tests := []struct { + name string + setup func() + addr sdk.AccAddress + cb func(orderID uint64, orderTypeByte byte) bool + expSeen []orderIterCBArgs + }{ + { + name: "empty state", + addr: s.addr1, + expSeen: nil, + }, + { + name: "no orders for addr", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr2, + expSeen: nil, + }, + { + name: "one entry: ask", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr2, + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk)}, + }, + { + name: "one entry: bid", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 7), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr2, + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeBid)}, + }, + { + name: "one entry no value", + setup: func() { + s.getStore().Set(keeper.MakeIndexKeyAddressToOrder(s.addr1, 4), []byte{}) + }, + addr: s.addr1, + expSeen: nil, + }, + { + name: "one entry bad key", + setup: func() { + s.getStore().Set(s.badKey(keeper.MakeIndexKeyAddressToOrder(s.addr1, 4)), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr1, + expSeen: nil, + }, + { + name: "five entries, 1 through 5: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 5), []byte{keeper.OrderKeyTypeBid}) + }, + addr: s.addr4, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(5, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, 1 through 5: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr4, 5), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr4, + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(1, keeper.OrderKeyTypeBid)}, + }, + { + name: "five entries, 1 through 5: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr2, 5), []byte{keeper.OrderKeyTypeBid}) + }, + addr: s.addr2, + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk), + }, + }, + { + name: "five entries, random: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr5, 44), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr5, 96), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr5, 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr5, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr5, 56), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr5, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(56, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(75, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(96, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, random: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 56), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr3, + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk)}, + }, + { + name: "five entries, random: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 75), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 56), []byte{keeper.OrderKeyTypeBid}) + }, + addr: s.addr3, + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(56, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries: two are bad", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 2), []byte{}) + store.Set(s.badKey(keeper.MakeIndexKeyAddressToOrder(s.addr3, 3)), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAddressToOrder(s.addr3, 5), []byte{keeper.OrderKeyTypeAsk}) + }, + addr: s.addr3, + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(4, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(5, keeper.OrderKeyTypeAsk), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.cb == nil { + tc.cb = getAll + } + + seen = nil + testFunc := func() { + s.k.IterateAddressOrders(s.ctx, tc.addr, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateAddressOrders(%s)", s.getAddrName(tc.addr)) + assertEqualSlice(s, tc.expSeen, seen, orderIterCBArgs.orderIDString, "args provided to callback") + }) + } +} + +func (s *TestSuite) TestKeeper_IterateAssetOrders() { + var seen []orderIterCBArgs + getAll := func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return false + } + stopAfter := func(count int) func(orderID uint64, orderTypeByte byte) bool { + return func(orderID uint64, orderTypeByte byte) bool { + seen = append(seen, newOrderIterCBArgs(orderID, orderTypeByte)) + return len(seen) >= count + } + } + + tests := []struct { + name string + setup func() + assetDenom string + cb func(orderID uint64, orderTypeByte byte) bool + expSeen []orderIterCBArgs + }{ + { + name: "empty state", + assetDenom: "apple", + expSeen: nil, + }, + { + name: "no orders for addr", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 7), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "banana", + expSeen: nil, + }, + { + name: "one entry: ask", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("banana", 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 7), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "banana", + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk)}, + }, + { + name: "one entry: bid", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("apple", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("banana", 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 5), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 6), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("cactus", 7), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "banana", + expSeen: []orderIterCBArgs{newOrderIterCBArgs(4, keeper.OrderKeyTypeBid)}, + }, + { + name: "one entry no value", + setup: func() { + s.getStore().Set(keeper.MakeIndexKeyAssetToOrder("banana", 4), []byte{}) + }, + assetDenom: "banana", + expSeen: nil, + }, + { + name: "one entry bad key", + setup: func() { + s.getStore().Set(s.badKey(keeper.MakeIndexKeyAssetToOrder("banana", 4)), []byte{keeper.OrderKeyTypeBid}) + }, + assetDenom: "banana", + expSeen: nil, + }, + { + name: "five entries, 1 through 5: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 4), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 5), []byte{keeper.OrderKeyTypeBid}) + }, + assetDenom: "acorn", + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(4, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(5, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, 1 through 5: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 1), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 2), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 5), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "acorn", + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(1, keeper.OrderKeyTypeBid)}, + }, + { + name: "five entries, 1 through 5: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 2), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("acorn", 5), []byte{keeper.OrderKeyTypeBid}) + }, + assetDenom: "acorn", + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(2, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk), + }, + }, + { + name: "five entries, random: get all", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 44), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 96), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 56), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "raspberry", + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(56, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(75, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(96, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries, random: get one", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 75), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 3), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("raspberry", 56), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "raspberry", + cb: stopAfter(1), + expSeen: []orderIterCBArgs{newOrderIterCBArgs(3, keeper.OrderKeyTypeAsk)}, + }, + { + name: "five entries, random: get three", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 44), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 96), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 75), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 3), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 56), []byte{keeper.OrderKeyTypeBid}) + }, + assetDenom: "huckleberry", + cb: stopAfter(3), + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(3, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(44, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(56, keeper.OrderKeyTypeBid), + }, + }, + { + name: "five entries: two are bad", + setup: func() { + store := s.getStore() + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 1), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 2), []byte{}) + store.Set(s.badKey(keeper.MakeIndexKeyAssetToOrder("huckleberry", 3)), []byte{keeper.OrderKeyTypeAsk}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 4), []byte{keeper.OrderKeyTypeBid}) + store.Set(keeper.MakeIndexKeyAssetToOrder("huckleberry", 5), []byte{keeper.OrderKeyTypeAsk}) + }, + assetDenom: "huckleberry", + expSeen: []orderIterCBArgs{ + newOrderIterCBArgs(1, keeper.OrderKeyTypeAsk), + newOrderIterCBArgs(4, keeper.OrderKeyTypeBid), + newOrderIterCBArgs(5, keeper.OrderKeyTypeAsk), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if tc.setup != nil { + tc.setup() + } + + if tc.cb == nil { + tc.cb = getAll + } + + seen = nil + testFunc := func() { + s.k.IterateAssetOrders(s.ctx, tc.assetDenom, tc.cb) + } + s.Require().NotPanics(testFunc, "IterateAssetOrders(%q)", tc.assetDenom) + s.Assert().Equal(tc.expSeen, seen, "args provided to callback") + assertEqualSlice(s, tc.expSeen, seen, orderIterCBArgs.orderIDString, "args provided to callback") + }) + } +} diff --git a/x/exchange/keeper/params_test.go b/x/exchange/keeper/params_test.go index 21a49d4b71..0d9f783e6b 100644 --- a/x/exchange/keeper/params_test.go +++ b/x/exchange/keeper/params_test.go @@ -1,9 +1,331 @@ package keeper_test -// TODO[1658]: func (s *TestSuite) TestKeeper_SetParams() +import ( + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) -// TODO[1658]: func (s *TestSuite) TestKeeper_GetParams() +func (s *TestSuite) TestKeeper_SetParams() { + expEntry := func(denom string, value uint16) string { + keyBz := append([]byte{0}, []byte("split"+denom)...) + valueBz := keeper.Uint16Bz(value) + return s.stateEntryString(keyBz, valueBz) + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetParamsOrDefaults() + tests := []struct { + name string + params *exchange.Params + expState []string + }{ + { + name: "nil params", + params: nil, + expState: nil, + }, + { + name: "default params", + params: exchange.DefaultParams(), + expState: []string{expEntry("", uint16(exchange.DefaultDefaultSplit))}, + }, + { + name: "zero default and two specifics", + params: &exchange.Params{ + DefaultSplit: 0, + DenomSplits: []exchange.DenomSplit{ + {Denom: "cows", Split: 2000}, + {Denom: "chickens", Split: 255}, + }, + }, + expState: []string{ + expEntry("", 0), + expEntry("chickens", 255), + expEntry("cows", 2000), + }, + }, + { + name: "a default and four specifics", + params: &exchange.Params{ + DefaultSplit: 300, + DenomSplits: []exchange.DenomSplit{ + {Denom: "horses", Split: 500}, + {Denom: "llamas", Split: 800}, + {Denom: "pigs", Split: 1200}, + {Denom: "emus", Split: 9999}, + }, + }, + expState: []string{ + expEntry("", 300), + expEntry("emus", 9999), + expEntry("horses", 500), + expEntry("llamas", 800), + expEntry("pigs", 1200), + }, + }, + { + // This one also tests that previously set entries are deleted. + name: "one split", + params: &exchange.Params{ + DefaultSplit: 406, + DenomSplits: []exchange.DenomSplit{{Denom: "cats", Split: 5}}, + }, + expState: []string{ + expEntry("", 406), + expEntry("cats", 5), + }, + }, + { + // This one also tests that previously set entries are deleted. + name: "nil params again", + params: nil, + expState: nil, + }, + } -// TODO[1658]: func (s *TestSuite) TestKeeper_GetExchangeSplit() + s.clearExchangeState() + for _, tc := range tests { + s.Run(tc.name, func() { + testFunc := func() { + s.k.SetParams(s.ctx, tc.params) + } + s.Require().NotPanics(testFunc, "SetParams") + state := s.dumpExchangeState() + s.Assert().Equal(tc.expState, state, "state after SetParams") + }) + } +} + +func (s *TestSuite) TestKeeper_GetParams() { + tests := []struct { + name string + splits []exchange.DenomSplit + exp *exchange.Params + }{ + { + name: "empty state", + splits: nil, + exp: nil, + }, + { + name: "just a default", + splits: []exchange.DenomSplit{{Denom: "", Split: 444}}, + exp: &exchange.Params{DefaultSplit: 444}, + }, + { + name: "default and three splits", + splits: []exchange.DenomSplit{ + {Denom: "", Split: 432}, + {Denom: "pigs", Split: 550}, + {Denom: "chickens", Split: 2000}, + {Denom: "cows", Split: 98}, + }, + exp: &exchange.Params{ + DefaultSplit: 432, + DenomSplits: []exchange.DenomSplit{ + {Denom: "chickens", Split: 2000}, + {Denom: "cows", Split: 98}, + {Denom: "pigs", Split: 550}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.clearExchangeState() + if len(tc.splits) > 0 { + store := s.getStore() + for _, split := range tc.splits { + keeper.SetParamsSplit(store, split.Denom, uint16(split.Split)) + } + } + + var actual *exchange.Params + testFunc := func() { + actual = s.k.GetParams(s.ctx) + } + s.Require().NotPanics(testFunc, "GetParams()") + s.Assert().Equal(tc.exp, actual, "GetParams() result") + }) + } +} + +func (s *TestSuite) TestKeeper_GetParamsOrDefaults() { + tests := []struct { + name string + params *exchange.Params + exp *exchange.Params + }{ + { + name: "no params", + params: nil, + exp: exchange.DefaultParams(), + }, + { + name: "zero default no splits", + params: &exchange.Params{DefaultSplit: 0}, + exp: &exchange.Params{DefaultSplit: 0}, + }, + { + name: "zero default two splits", + params: &exchange.Params{ + DefaultSplit: 0, + DenomSplits: []exchange.DenomSplit{ + {Denom: "llamas", Split: 222}, + {Denom: "cows", Split: 88}, + }, + }, + exp: &exchange.Params{ + DefaultSplit: 0, + DenomSplits: []exchange.DenomSplit{ + {Denom: "cows", Split: 88}, + {Denom: "llamas", Split: 222}, + }, + }, + }, + { + name: "non-zero default and no splits", + params: &exchange.Params{DefaultSplit: 510}, + exp: &exchange.Params{DefaultSplit: 510}, + }, + { + name: "non-zero default and two splits", + params: &exchange.Params{ + DefaultSplit: 3333, + DenomSplits: []exchange.DenomSplit{ + {Denom: "pigs", Split: 111}, + {Denom: "chickens", Split: 72}, + }, + }, + exp: &exchange.Params{ + DefaultSplit: 3333, + DenomSplits: []exchange.DenomSplit{ + {Denom: "chickens", Split: 72}, + {Denom: "pigs", Split: 111}, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.k.SetParams(s.ctx, tc.params) + + var actual *exchange.Params + testFunc := func() { + actual = s.k.GetParamsOrDefaults(s.ctx) + } + s.Require().NotPanics(testFunc, "GetParamsOrDefaults()") + s.Assert().Equal(tc.exp, actual, "GetParamsOrDefaults() result") + }) + } +} + +func (s *TestSuite) TestKeeper_GetExchangeSplit() { + defaultSplit := uint16(exchange.DefaultDefaultSplit) + tests := []struct { + name string + params *exchange.Params + denom string + exp uint16 + }{ + { + name: "no params, empty string", + params: nil, + denom: "", + exp: defaultSplit, + }, + { + name: "no params, chickens", + params: nil, + denom: "chickens", + exp: defaultSplit, + }, + { + name: "default params, empty string", + params: exchange.DefaultParams(), + denom: "", + exp: defaultSplit, + }, + { + name: "default params, cows", + params: exchange.DefaultParams(), + denom: "cows", + exp: defaultSplit, + }, + { + name: "split for llamas, emus", + params: &exchange.Params{ + DefaultSplit: 300, + DenomSplits: []exchange.DenomSplit{{Denom: "llamas", Split: 100}}, + }, + denom: "emus", + exp: 300, + }, + { + name: "split for llamas, llama (not plural)", + params: &exchange.Params{ + DefaultSplit: 300, + DenomSplits: []exchange.DenomSplit{{Denom: "llamas", Split: 100}}, + }, + denom: "llama", + exp: 300, + }, + { + name: "split for llamas, llamas", + params: &exchange.Params{ + DefaultSplit: 300, + DenomSplits: []exchange.DenomSplit{{Denom: "llamas", Split: 100}}, + }, + denom: "llamas", + exp: 100, + }, + { + name: "splits for cows, chickens: pigs", + params: &exchange.Params{ + DefaultSplit: 200, + DenomSplits: []exchange.DenomSplit{ + {Denom: "chickens", Split: 300}, + {Denom: "cows", Split: 400}, + }, + }, + denom: "pigs", + exp: 200, + }, + { + name: "splits for cows, chickens: cows", + params: &exchange.Params{ + DefaultSplit: 200, + DenomSplits: []exchange.DenomSplit{ + {Denom: "chickens", Split: 300}, + {Denom: "cows", Split: 400}, + }, + }, + denom: "cows", + exp: 400, + }, + { + name: "splits for cows, chickens: chickens", + params: &exchange.Params{ + DefaultSplit: 200, + DenomSplits: []exchange.DenomSplit{ + {Denom: "chickens", Split: 300}, + {Denom: "cows", Split: 400}, + }, + }, + denom: "chickens", + exp: 300, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.k.SetParams(s.ctx, tc.params) + var actual uint16 + testFunc := func() { + actual = s.k.GetExchangeSplit(s.ctx, tc.denom) + } + s.Require().NotPanics(testFunc, "GetExchangeSplit(%q)", tc.denom) + s.Assert().Equal(tc.exp, actual, "GetExchangeSplit(%q) result", tc.denom) + }) + } +} diff --git a/x/exchange/keeper/suite_test.go b/x/exchange/keeper/suite_test.go new file mode 100644 index 0000000000..f1606f520f --- /dev/null +++ b/x/exchange/keeper/suite_test.go @@ -0,0 +1,670 @@ +package keeper_test + +import ( + "bytes" + "fmt" + "sort" + "strings" + "testing" + + "github.com/gogo/protobuf/proto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/suite" + + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/keeper" +) + +type TestSuite struct { + suite.Suite + + app *app.App + ctx sdk.Context + + k keeper.Keeper + + addr1 sdk.AccAddress + addr2 sdk.AccAddress + addr3 sdk.AccAddress + addr4 sdk.AccAddress + addr5 sdk.AccAddress + + marketAddr1 sdk.AccAddress + marketAddr2 sdk.AccAddress + marketAddr3 sdk.AccAddress + + feeCollector string + feeCollectorAddr sdk.AccAddress + + accKeeper *MockAccountKeeper + + logBuffer bytes.Buffer +} + +func (s *TestSuite) SetupTest() { + bufferedLoggerMaker := func() log.Logger { + lw := zerolog.ConsoleWriter{ + Out: &s.logBuffer, + NoColor: true, + PartsExclude: []string{"time"}, // Without this, each line starts with " " + } + // Error log lines will start with "ERR ". + // Info log lines will start with "INF ". + // Debug log lines are omitted, but would start with "DBG ". + logger := zerolog.New(lw).Level(zerolog.InfoLevel) + return server.ZeroLogWrapper{Logger: logger} + } + // swap in the buffered logger maker so it's used in app.Setup, but then put it back (since that's a global thing). + defer app.SetLoggerMaker(app.SetLoggerMaker(bufferedLoggerMaker)) + + s.app = app.Setup(s.T()) + s.logBuffer.Reset() + s.ctx = s.app.BaseApp.NewContext(false, tmproto.Header{}) + s.k = s.app.ExchangeKeeper + + addrs := app.AddTestAddrsIncremental(s.app, s.ctx, 5, sdk.NewInt(1_000_000_000)) + s.addr1 = addrs[0] + s.addr2 = addrs[1] + s.addr3 = addrs[2] + s.addr4 = addrs[3] + s.addr5 = addrs[4] + + s.marketAddr1 = exchange.GetMarketAddress(1) + s.marketAddr2 = exchange.GetMarketAddress(2) + s.marketAddr3 = exchange.GetMarketAddress(3) + + s.feeCollector = s.k.GetFeeCollectorName() + s.feeCollectorAddr = authtypes.NewModuleAddress(s.feeCollector) +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +// sliceStrings converts each val into a string using the provided stringer, prefixing the slice index to each. +func sliceStrings[T any](vals []T, stringer func(T) string) []string { + if vals == nil { + return nil + } + strs := make([]string, len(vals)) + for i, v := range vals { + strs[i] = fmt.Sprintf("[%d]:%s", i, stringer(v)) + } + return strs +} + +// sliceString converts each val into a string using the provided stringer with the index prefixed to it, and joins them with ", ". +func sliceString[T any](vals []T, stringer func(T) string) string { + if vals == nil { + return "" + } + return strings.Join(sliceStrings(vals, stringer), ", ") +} + +// copySlice returns a copy of a slice using the provided copier for each value. +func copySlice[T any](vals []T, copier func(T) T) []T { + if vals == nil { + return nil + } + rv := make([]T, len(vals)) + for i, v := range vals { + rv[i] = copier(v) + } + return rv +} + +// noOpCopier is a passthrough "copier" function that just returns the exact same thing that was provided. +func noOpCopier[T any](val T) T { + return val +} + +// reverseSlice returns a new slice with the entries reversed. +func reverseSlice[T any](vals []T) []T { + if vals == nil { + return nil + } + rv := make([]T, len(vals)) + for i, val := range vals { + rv[len(vals)-i-1] = val + } + return rv +} + +// getLogOutput gets the log buffer contents. This (probably) also clears the log buffer. +func (s *TestSuite) getLogOutput(msg string, args ...interface{}) string { + logOutput := s.logBuffer.String() + s.T().Logf(msg+" log output:\n%s", append(args, logOutput)...) + return logOutput +} + +// badKey creates a copy of the provided key, moves the last byte to the 2nd to last, +// then chops off the last byte (so the result is one byte shorter). +func (s *TestSuite) badKey(key []byte) []byte { + rv := make([]byte, len(key)-1) + copy(rv, key) + rv[len(rv)-1] = key[len(key)-1] + return rv +} + +// coins creates a new sdk.Coins from a string, requiring it to work. +func (s *TestSuite) coins(coins string) sdk.Coins { + s.T().Helper() + rv, err := sdk.ParseCoinsNormalized(coins) + s.Require().NoError(err, "ParseCoinsNormalized(%q)", coins) + return rv +} + +// coin creates a new coin from a string, requiring it to work. +func (s *TestSuite) coin(coin string) sdk.Coin { + rv, err := sdk.ParseCoinNormalized(coin) + s.Require().NoError(err, "ParseCoinNormalized(%q)", coin) + return rv +} + +// coinP creates a reference to a new coin from a string, requiring it to work. +func (s *TestSuite) coinP(coin string) *sdk.Coin { + rv := s.coin(coin) + return &rv +} + +// coinsString converts a slice of coin entries into a string. +// This is different from sdk.Coins.String because the entries aren't sorted in here. +func (s *TestSuite) coinsString(coins []sdk.Coin) string { + return sliceString(coins, func(coin sdk.Coin) string { + return fmt.Sprintf("%q", coin) + }) +} + +// coinPString converts the provided coin to a quoted string, or "". +func (s *TestSuite) coinPString(coin *sdk.Coin) string { + if coin == nil { + return "" + } + return fmt.Sprintf("%q", coin) +} + +// ratio creates a FeeRatio from a ":" string. +func (s *TestSuite) ratio(ratioStr string) exchange.FeeRatio { + rv, err := exchange.ParseFeeRatio(ratioStr) + s.Require().NoError(err, "ParseFeeRatio(%q)", ratioStr) + return *rv +} + +// ratios creates a slice of Fee ratio from a comma delimited list of ":" entries in a string. +func (s *TestSuite) ratios(ratiosStr string) []exchange.FeeRatio { + if len(ratiosStr) == 0 { + return nil + } + + ratios := strings.Split(ratiosStr, ",") + rv := make([]exchange.FeeRatio, len(ratios)) + for i, r := range ratios { + rv[i] = s.ratio(r) + } + return rv +} + +// ratiosStrings converts the ratios into strings. It's because comparsions on sdk.Coin (or sdkmath.Int) are annoying. +func (s *TestSuite) ratiosStrings(ratios []exchange.FeeRatio) []string { + return sliceStrings(ratios, exchange.FeeRatio.String) +} + +// joinErrs joins the provided error strings into a single one to match what errors.Join does. +func (s *TestSuite) joinErrs(errs ...string) string { + return strings.Join(errs, "\n") +} + +// copyCoin creates a copy of a coin (as best as possible). +func (s *TestSuite) copyCoin(orig sdk.Coin) sdk.Coin { + return sdk.NewCoin(orig.Denom, orig.Amount.AddRaw(0)) +} + +// copyCoinP copies a coin that's a reference. +func (s *TestSuite) copyCoinP(orig *sdk.Coin) *sdk.Coin { + if orig == nil { + return nil + } + rv := s.copyCoin(*orig) + return &rv +} + +// copyCoins creates a copy of coins (as best as possible). +func (s *TestSuite) copyCoins(orig []sdk.Coin) []sdk.Coin { + return copySlice(orig, s.copyCoin) +} + +// copyRatio creates a copy of a FeeRatio. +func (s *TestSuite) copyRatio(orig exchange.FeeRatio) exchange.FeeRatio { + return exchange.FeeRatio{ + Price: s.copyCoin(orig.Price), + Fee: s.copyCoin(orig.Fee), + } +} + +// copyRatios creates a copy of a slice of FeeRatios. +func (s *TestSuite) copyRatios(orig []exchange.FeeRatio) []exchange.FeeRatio { + return copySlice(orig, s.copyRatio) +} + +// copyAccessGrant creates a copy of an AccessGrant. +func (s *TestSuite) copyAccessGrant(orig exchange.AccessGrant) exchange.AccessGrant { + return exchange.AccessGrant{ + Address: orig.Address, + Permissions: copySlice(orig.Permissions, noOpCopier[exchange.Permission]), + } +} + +// copyAccessGrants creates a copy of a slice of AccessGrants. +func (s *TestSuite) copyAccessGrants(orig []exchange.AccessGrant) []exchange.AccessGrant { + return copySlice(orig, s.copyAccessGrant) +} + +// copyStrings creates a copy of a slice of strings. +func (s *TestSuite) copyStrings(orig []string) []string { + return copySlice(orig, noOpCopier[string]) +} + +// copyMarket creates a deep copy of a market. +func (s *TestSuite) copyMarket(orig exchange.Market) exchange.Market { + return exchange.Market{ + MarketId: orig.MarketId, + MarketDetails: exchange.MarketDetails{ + Name: orig.MarketDetails.Name, + Description: orig.MarketDetails.Description, + WebsiteUrl: orig.MarketDetails.WebsiteUrl, + IconUri: orig.MarketDetails.IconUri, + }, + FeeCreateAskFlat: s.copyCoins(orig.FeeCreateAskFlat), + FeeCreateBidFlat: s.copyCoins(orig.FeeCreateBidFlat), + FeeSellerSettlementFlat: s.copyCoins(orig.FeeSellerSettlementFlat), + FeeSellerSettlementRatios: s.copyRatios(orig.FeeSellerSettlementRatios), + FeeBuyerSettlementFlat: s.copyCoins(orig.FeeBuyerSettlementFlat), + FeeBuyerSettlementRatios: s.copyRatios(orig.FeeBuyerSettlementRatios), + AcceptingOrders: orig.AcceptingOrders, + AllowUserSettlement: orig.AllowUserSettlement, + AccessGrants: s.copyAccessGrants(orig.AccessGrants), + ReqAttrCreateAsk: s.copyStrings(orig.ReqAttrCreateAsk), + ReqAttrCreateBid: s.copyStrings(orig.ReqAttrCreateBid), + } +} + +// copyMarkets creates a copy of a slice of markets. +func (s *TestSuite) copyMarkets(orig []exchange.Market) []exchange.Market { + return copySlice(orig, s.copyMarket) +} + +// copyOrder creates a copy of an order. +func (s *TestSuite) copyOrder(orig exchange.Order) exchange.Order { + rv := exchange.NewOrder(orig.OrderId) + switch { + case orig.IsAskOrder(): + rv.WithAsk(s.copyAskOrder(orig.GetAskOrder())) + case orig.IsBidOrder(): + rv.WithBid(s.copyBidOrder(orig.GetBidOrder())) + default: + rv.Order = orig.Order + } + return *rv +} + +// copyOrders creates a copy of a slice of orders. +func (s *TestSuite) copyOrders(orig []exchange.Order) []exchange.Order { + return copySlice(orig, s.copyOrder) +} + +// copyAskOrder creates a copy of an AskOrder. +func (s *TestSuite) copyAskOrder(orig *exchange.AskOrder) *exchange.AskOrder { + if orig == nil { + return nil + } + return &exchange.AskOrder{ + MarketId: orig.MarketId, + Seller: orig.Seller, + Assets: s.copyCoin(orig.Assets), + Price: s.copyCoin(orig.Price), + SellerSettlementFlatFee: s.copyCoinP(orig.SellerSettlementFlatFee), + AllowPartial: orig.AllowPartial, + ExternalId: orig.ExternalId, + } +} + +// copyBidOrder creates a copy of a BidOrder. +func (s *TestSuite) copyBidOrder(orig *exchange.BidOrder) *exchange.BidOrder { + if orig == nil { + return nil + } + return &exchange.BidOrder{ + MarketId: orig.MarketId, + Buyer: orig.Buyer, + Assets: s.copyCoin(orig.Assets), + Price: s.copyCoin(orig.Price), + BuyerSettlementFees: s.copyCoins(orig.BuyerSettlementFees), + AllowPartial: orig.AllowPartial, + ExternalId: orig.ExternalId, + } +} + +// untypeEvent applies sdk.TypedEventToEvent(tev) requiring it to not error. +func (s *TestSuite) untypeEvent(tev proto.Message) sdk.Event { + rv, err := sdk.TypedEventToEvent(tev) + s.Require().NoError(err, "TypedEventToEvent(%T)", tev) + return rv +} + +// untypeEvents applies sdk.TypedEventToEvent(tev) to each of the provided things, requiring it to not error. +func untypeEvents[P proto.Message](s *TestSuite, tevs []P) sdk.Events { + rv := make(sdk.Events, len(tevs)) + for i, tev := range tevs { + event, err := sdk.TypedEventToEvent(tev) + s.Require().NoError(err, "[%d]TypedEventToEvent(%T)", i, tev) + rv[i] = event + } + return rv +} + +// creates a copy of a DenomSplit. +func (s *TestSuite) copyDenomSplit(orig exchange.DenomSplit) exchange.DenomSplit { + return exchange.DenomSplit{ + Denom: orig.Denom, + Split: orig.Split, + } +} + +// copyDenomSplits creates a copy of a slice of DenomSplits. +func (s *TestSuite) copyDenomSplits(orig []exchange.DenomSplit) []exchange.DenomSplit { + return copySlice(orig, s.copyDenomSplit) +} + +// copyParams creates a copy of exchange Params. +func (s *TestSuite) copyParams(orig *exchange.Params) *exchange.Params { + if orig == nil { + return nil + } + return &exchange.Params{ + DefaultSplit: orig.DefaultSplit, + DenomSplits: s.copyDenomSplits(orig.DenomSplits), + } +} + +// copyGenState creates a copy of a GenesisState. +func (s *TestSuite) copyGenState(genState *exchange.GenesisState) *exchange.GenesisState { + if genState == nil { + return nil + } + return &exchange.GenesisState{ + Params: s.copyParams(genState.Params), + Markets: s.copyMarkets(genState.Markets), + Orders: s.copyOrders(genState.Orders), + LastMarketId: genState.LastMarketId, + LastOrderId: genState.LastOrderId, + } +} + +// sortMarket sorts all the fields in a market. +func (s *TestSuite) sortMarket(market *exchange.Market) *exchange.Market { + if len(market.FeeSellerSettlementRatios) > 0 { + sort.Slice(market.FeeSellerSettlementRatios, func(i, j int) bool { + if market.FeeSellerSettlementRatios[i].Price.Denom < market.FeeSellerSettlementRatios[j].Price.Denom { + return true + } + if market.FeeSellerSettlementRatios[i].Price.Denom > market.FeeSellerSettlementRatios[j].Price.Denom { + return false + } + return market.FeeSellerSettlementRatios[i].Fee.Denom < market.FeeSellerSettlementRatios[j].Fee.Denom + }) + } + if len(market.FeeBuyerSettlementRatios) > 0 { + sort.Slice(market.FeeBuyerSettlementRatios, func(i, j int) bool { + if market.FeeBuyerSettlementRatios[i].Price.Denom < market.FeeBuyerSettlementRatios[j].Price.Denom { + return true + } + if market.FeeBuyerSettlementRatios[i].Price.Denom > market.FeeBuyerSettlementRatios[j].Price.Denom { + return false + } + return market.FeeBuyerSettlementRatios[i].Fee.Denom < market.FeeBuyerSettlementRatios[j].Fee.Denom + }) + } + if len(market.AccessGrants) > 0 { + sort.Slice(market.AccessGrants, func(i, j int) bool { + // Horribly inefficient. Not meant for production. + addrI, err := sdk.AccAddressFromBech32(market.AccessGrants[i].Address) + s.Require().NoError(err, "AccAddressFromBech32(%q)", market.AccessGrants[i].Address) + addrJ, err := sdk.AccAddressFromBech32(market.AccessGrants[j].Address) + s.Require().NoError(err, "AccAddressFromBech32(%q)", market.AccessGrants[j].Address) + return bytes.Compare(addrI, addrJ) < 0 + }) + for _, ag := range market.AccessGrants { + sort.Slice(ag.Permissions, func(i, j int) bool { + return ag.Permissions[i] < ag.Permissions[j] + }) + } + } + return market +} + +// sortGenState sorts the contents of a GenesisState. +func (s *TestSuite) sortGenState(genState *exchange.GenesisState) *exchange.GenesisState { + if genState == nil { + return nil + } + if genState.Params != nil && len(genState.Params.DenomSplits) > 0 { + sort.Slice(genState.Params.DenomSplits, func(i, j int) bool { + return genState.Params.DenomSplits[i].Denom < genState.Params.DenomSplits[j].Denom + }) + } + if len(genState.Markets) > 0 { + sort.Slice(genState.Markets, func(i, j int) bool { + return genState.Markets[i].MarketId < genState.Markets[j].MarketId + }) + for _, market := range genState.Markets { + s.sortMarket(&market) + } + } + if len(genState.Orders) > 0 { + sort.Slice(genState.Orders, func(i, j int) bool { + return genState.Orders[i].OrderId < genState.Orders[j].OrderId + }) + } + return genState +} + +// getOrderIDStr gets a string of the given order's id. +func (s *TestSuite) getOrderIDStr(order *exchange.Order) string { + if order == nil { + return "" + } + return fmt.Sprintf("%d", order.OrderId) +} + +// agCanOnly creates an AccessGrant for the given address with only the provided permission. +func (s *TestSuite) agCanOnly(addr sdk.AccAddress, perm exchange.Permission) exchange.AccessGrant { + return exchange.AccessGrant{ + Address: addr.String(), + Permissions: []exchange.Permission{perm}, + } +} + +// agCanAllBut creates an AccessGrant for the given address with all permissions except the provided one. +func (s *TestSuite) agCanAllBut(addr sdk.AccAddress, perm exchange.Permission) exchange.AccessGrant { + rv := exchange.AccessGrant{ + Address: addr.String(), + } + for _, p := range exchange.AllPermissions() { + if p != perm { + rv.Permissions = append(rv.Permissions, p) + } + } + return rv +} + +// agCanEverything creates an AccessGrant for the given address with all permissions available. +func (s *TestSuite) agCanEverything(addr sdk.AccAddress) exchange.AccessGrant { + return exchange.AccessGrant{ + Address: addr.String(), + Permissions: exchange.AllPermissions(), + } +} + +// getAddrName returns the name of the variable in this TestSuite holding the provided address. +func (s *TestSuite) getAddrName(addr sdk.AccAddress) string { + switch string(addr) { + case string(s.addr1): + return "addr1" + case string(s.addr2): + return "addr2" + case string(s.addr3): + return "addr3" + case string(s.addr4): + return "addr4" + case string(s.addr5): + return "addr5" + case string(s.marketAddr1): + return "marketAddr1" + case string(s.marketAddr2): + return "marketAddr2" + case string(s.marketAddr3): + return "marketAddr3" + case string(s.feeCollectorAddr): + return "feeCollectorAddr" + default: + return addr.String() + } +} + +// getAddrStrName returns the name of the variable in this TestSuite holding the provided address. +func (s *TestSuite) getAddrStrName(addrStr string) string { + addr, err := sdk.AccAddressFromBech32(addrStr) + if err != nil { + return addrStr + } + return s.getAddrName(addr) +} + +// getStore gets the exchange store. +func (s *TestSuite) getStore() sdk.KVStore { + return s.k.GetStore(s.ctx) +} + +// clearExchangeState deletes everything from the exchange state store. +func (s *TestSuite) clearExchangeState() { + keeper.DeleteAll(s.getStore(), nil) + s.accKeeper = nil +} + +// stateEntryString converts the provided key and value into a ""="" string. +func (s *TestSuite) stateEntryString(key, value []byte) string { + return fmt.Sprintf("%q=%q", key, value) +} + +// dumpExchangeState creates a string for each entry in the hold state store. +// Each entry has the format `""=""`. +func (s *TestSuite) dumpExchangeState() []string { + var rv []string + keeper.Iterate(s.getStore(), nil, func(key, value []byte) bool { + rv = append(rv, s.stateEntryString(key, value)) + return false + }) + return rv +} + +// requireSetOrderInStore calls SetOrderInStore making sure it doesn't panic or return an error. +func (s *TestSuite) requireSetOrderInStore(store sdk.KVStore, order *exchange.Order) { + assertions.RequireNotPanicsNoErrorf(s.T(), func() error { + return s.k.SetOrderInStore(store, *order) + }, "SetOrderInStore(%d)", order.OrderId) +} + +// requireCreateMarket calls CreateMarket making sure it doesn't panic or return an error. +// It also uses the TestSuite.accKeeper for the market account. +func (s *TestSuite) requireCreateMarket(market exchange.Market) { + if s.accKeeper == nil { + s.accKeeper = NewMockAccountKeeper() + } + assertions.RequireNotPanicsNoErrorf(s.T(), func() error { + _, err := s.k.WithAccountKeeper(s.accKeeper).CreateMarket(s.ctx, market) + return err + }, "CreateMarket(%d)", market.MarketId) +} + +// requireCreateMarketUnmocked calls CreateMarket making sure it doesn't panic or return an error. +// This uses the normal account keeper (instead of a mocked one). +func (s *TestSuite) requireCreateMarketUnmocked(market exchange.Market) { + assertions.RequireNotPanicsNoErrorf(s.T(), func() error { + _, err := s.k.CreateMarket(s.ctx, market) + return err + }, "CreateMarket(%d)", market.MarketId) +} + +// assertEqualSlice asserts that expected = actual and returns true if so. +// If not, returns false and the stringer is applied to each entry and the comparison +// is redone on the strings in the hopes that it helps identify the problem. +// If the strings are also equal, each individual entry is compared. +func assertEqualSlice[T any](s *TestSuite, expected, actual []T, stringer func(T) string, msg string, args ...interface{}) bool { + s.T().Helper() + if s.Assert().Equalf(expected, actual, msg, args...) { + return true + } + // compare each as strings in the hopes that makes it easier to identify the problem. + expStrs := sliceStrings(expected, stringer) + actStrs := sliceStrings(actual, stringer) + if !s.Assert().Equalf(expStrs, actStrs, "strings: "+msg, args...) { + return false + } + // They're the same as strings, so compare each individually. + for i := range expected { + s.Assert().Equalf(expected[i], actual[i], msg+fmt.Sprintf("[%d]", i), args...) + } + return false +} + +// assertEqualOrderID asserts that two uint64 values are equal, and if not, includes their decimal form in the log. +// This is nice because .Equal failures output uints in hex, which can make it difficult to identify what's going on. +func (s *TestSuite) assertEqualOrderID(expected, actual uint64, msgAndArgs ...interface{}) bool { + s.T().Helper() + if s.Assert().Equal(expected, actual, msgAndArgs...) { + return true + } + s.T().Logf("Expected order id: %d", expected) + s.T().Logf(" Actual order id: %d", actual) + return false +} + +// assertEqualOrders asserts that the slices of orders are equal. +// If not, some further assertions are made to try to help try to clarify the differences. +func (s *TestSuite) assertEqualOrders(expected, actual []*exchange.Order, msg string, args ...interface{}) bool { + s.T().Helper() + return assertEqualSlice(s, expected, actual, s.getOrderIDStr, msg, args...) +} + +// assertErrorValue is a wrapper for assertions.AssertErrorValue for this TestSuite. +func (s *TestSuite) assertErrorValue(theError error, expected string, msgAndArgs ...interface{}) bool { + s.T().Helper() + return assertions.AssertErrorValue(s.T(), theError, expected, msgAndArgs...) +} + +// assertErrorContentsf is a wrapper for assertions.AssertErrorContentsf for this TestSuite. +func (s *TestSuite) assertErrorContentsf(theError error, contains []string, msg string, args ...interface{}) bool { + s.T().Helper() + return assertions.AssertErrorContentsf(s.T(), theError, contains, msg, args...) +} + +// assertEqualEvents is a wrapper for assertions.AssertEqualEvents for this TestSuite. +func (s *TestSuite) assertEqualEvents(expected, actual sdk.Events, msgAndArgs ...interface{}) bool { + s.T().Helper() + return assertions.AssertEqualEvents(s.T(), expected, actual, msgAndArgs...) +} + +// requirePanicEquals is a wrapper for assertions.RequirePanicEquals for this TestSuite. +func (s *TestSuite) requirePanicEquals(f assertions.PanicTestFunc, expected string, msgAndArgs ...interface{}) { + s.T().Helper() + assertions.RequirePanicEquals(s.T(), f, expected, msgAndArgs...) +} diff --git a/x/exchange/market_test.go b/x/exchange/market_test.go index da4d04de7e..1e878bb46a 100644 --- a/x/exchange/market_test.go +++ b/x/exchange/market_test.go @@ -4213,6 +4213,12 @@ func TestIsReqAttrMatch(t *testing.T) { accAttr: "penny.dime.quarter.dollar", exp: false, }, + { + name: "with wildcard: just base", + reqAttr: "*.penny.dime.quarter", + accAttr: "penny.dime.quarter", + exp: false, + }, { name: "with wildcard: missing 1st char from 1st name", reqAttr: "*.penny.dime.quarter", diff --git a/x/exchange/params.go b/x/exchange/params.go index 25286a704c..79f77b5d89 100644 --- a/x/exchange/params.go +++ b/x/exchange/params.go @@ -9,7 +9,6 @@ import ( const ( // DefaultDefaultSplit is the default value used for the DefaultSplit parameter. - // TODO[1658]: Discuss what this should be with someone who would know. DefaultDefaultSplit = uint32(500) // MaxSplit is the maximum split value. 10,000 basis points = 100%.