From f84a50fd9b0d93edf450493b39bcbbb4da17fc7f Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 3 Nov 2023 09:56:27 -0600 Subject: [PATCH] Backport: arm64 docker container (#1709), exchange keeper tests and fixes (#1713) and some readme updates (#1722, #1725) (#1727) * Add arm64 to docker publish workflow (#1709) * add arm64 to docker publish workflow * add comments back to file * Add changelog entry * temp add of my branch * remove test * second attempt * remove test * add a fix and temp publish docker from branch * better logic * try setting arg another way * remove ARCH arg from build, use uname from container to copy over correct wasm file * remove temp wasm files * Add arch check and error message for clarity * remove custom branch from workflow * test build platform * remove cleveldb, check build archs * remove branch * Add Acknowledgement section to Oracle README. (#1722) * Add Acknowledgement section. * Add missing link. --------- Co-authored-by: Ira Miller <72319+iramiller@users.noreply.github.com> * Updates to readme from marketing (#1725) * 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. * Mark v1.17.0-rc2 in the changelog and release notes. * Undo the marking of v1.17.0-rc2 since we might have other stuff come in for it. * Move the 1634 changelog entry up in the unlreleased section where it should be for now. --------- Co-authored-by: Carlton Hanna Co-authored-by: Matt Witkowski Co-authored-by: Ira Miller <72319+iramiller@users.noreply.github.com> Co-authored-by: AJ Webb --- .github/workflows/docker.yml | 12 +- CHANGELOG.md | 22 +- Makefile | 7 +- README.md | 19 +- app/upgrades.go | 56 + app/upgrades_test.go | 14 + docker/blockchain/Dockerfile | 21 +- 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 - x/ibchooks/marker_hooks.go | 18 +- x/ibchooks/marker_hooks_test.go | 2 +- x/oracle/spec/README.md | 3 + 31 files changed, 20731 insertions(+), 558 deletions(-) create mode 100644 x/exchange/keeper/suite_test.go diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6b2ba6ada2..cae8de7780 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,7 +8,7 @@ on: tags: - "v[0-9]+.[0-9]+.[0-9]+" # Push events to matching v*, i.e. v1.0, v20.15.10 - "v[0-9]+.[0-9]+.[0-9]+-rc*" # Push events to matching v*, i.e. v1.0-rc1, v20.15.10-rc5 - + # Set concurrency for this workflow to cancel in-progress jobs if retriggered. # The github.ref is only available when triggered by a PR so fall back to github.run_id for other cases. # The github.run_id is unique for each run, giving each such invocation it's own unique concurrency group. @@ -17,7 +17,6 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true - jobs: docker: runs-on: ubuntu-latest @@ -25,13 +24,16 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Setup go uses: actions/setup-go@v4 with: go-version: '1.20' + - name: Go mod vendor run: | go mod vendor + - name: Prepare id: prep run: | @@ -56,16 +58,20 @@ jobs: created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" echo "Setting output: created=$created" echo "created=$created" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 + - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Publish to Docker Hub uses: docker/build-push-action@v5 with: @@ -73,7 +79,7 @@ jobs: target: run build-args: | VERSION=${{ steps.prep.outputs.version }} - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 file: docker/blockchain/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.prep.outputs.tags }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e69b91bfb2..86323f8e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,27 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] -* nothing +### Features + +* Add the `saffron-rc2` upgrade and update `saffron` to create denom metadata for IBC markers [#1728](https://github.com/provenance-io/provenance/issues/1728). + +### 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). +* Add publishing of docker arm64 container builds [#1634](https://github.com/provenance-io/provenance/issues/1634) + +### 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). + +### Full Commit History + +* https://github.com/provenance-io/provenance/compare/v1.17.0-rc1...v1.17.0-rc2 +* https://github.com/provenance-io/provenance/compare/v1.16.0...v1.17.0-rc2 --- diff --git a/Makefile b/Makefile index 41806cf750..43282b4424 100644 --- a/Makefile +++ b/Makefile @@ -454,11 +454,8 @@ vendor: # Full build inside a docker container for a clean release build docker-build: vendor -ifeq ($(UNAME_M),x86_64) - docker build --build-arg VERSION=$(VERSION) --build-arg ARCH=$(UNAME_M) -t provenance-io/blockchain . -f docker/blockchain/Dockerfile -else - docker build --build-arg VERSION=$(VERSION) --build-arg ARCH=aarch64 -t provenance-io/blockchain . -f docker/blockchain/Dockerfile -endif + docker build --build-arg VERSION=$(VERSION) -t provenance-io/blockchain . -f docker/blockchain/Dockerfile + # Quick build using local environment and go platform target options. docker-build-local: vendor diff --git a/README.md b/README.md index db3f4360ea..d993c4423d 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,11 @@ # Provenance Blockchain -[Provenance] is a distributed, proof of stake blockchain designed for the financial services industry. +[Provenance Blockchain][provenance] is an eco-friendly proof-of-stake (PoS) blockchain purpose built to modernize financial services, and the Provenance app is the core application for running a node in the network. -For more information about [Provenance Inc](https://provenance.io) visit https://provenance.io +Provenance Blockchain is open-source with purpose-built capabilities leveraging the [CometBFT](https://docs.cometbft.com) (formerly Tendermint) consensus engine and [Cosmos SDK][cosmos]. Combined, this complete financial services infrastructure makes it safer, easier, cheaper, and faster to build and manage financial products and services. - -The Provenance app is the core blockchain application for running a node on the Provenance Network. The node -software is based on the open source [Tendermint](https://tendermint.com) consensus engine combined with the -[Cosmos SDK](https://cosmos.network) and custom modules to support apis for financial services. [Figure](https://figure.com) -is the first and primary user of the Provenance Blockchain. +For more information on the many TradFi institutions and Fintechs leveraging the Provenance Blockchain ecosystem, and to learn more about the Provenance Blockchain Foundation, visit [https://provenance.io][provenance]. ## Status @@ -24,7 +20,6 @@ is the first and primary user of the Provenance Blockchain. [![LOC][loc-badge]][loc-report] ![Lint Status][lint-badge] - [license-badge]: https://img.shields.io/github/license/provenance-io/provenance.svg [license-url]: https://github.com/provenance-io/provenance/blob/main/LICENSE [release-badge]: https://img.shields.io/github/tag/provenance-io/provenance.svg @@ -36,14 +31,12 @@ is the first and primary user of the Provenance Blockchain. [loc-badge]: https://tokei.rs/b1/github/provenance-io/provenance [loc-report]: https://github.com/provenance-io/provenance [lint-badge]: https://github.com/provenance-io/provenance/workflows/Lint/badge.svg -[provenance]: https://provenance.io/#overview - -The Provenance networks are based on work from the private [Figure Technologies](https://figure.com) blockchain launched in 2018. +[provenance]: https://provenance.io/ +[cosmos]: https://cosmos.network/ ## Quick Start -The Provenance Blockchain is based on Cosmos, the [sdk introduction](https://github.com/cosmos/cosmos-sdk/blob/main/docs/docs/intro/00-overview.md) -is a useful starting point. +As the [Provenance Blockchain][provenance] and its core modules are based on the [Cosmos SDK][cosmos], [this introduction](https://docs.cosmos.network/main/learn/intro/overview) into Cosmos is a useful starting point. Developers can use a local checkout and the make targets `make run` and `make localnet-start` to run a local development network. diff --git a/app/upgrades.go b/app/upgrades.go index 74db97d190..dcd1945508 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "strings" icqtypes "github.com/strangelove-ventures/async-icq/v6/types" @@ -11,15 +12,18 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" attributekeeper "github.com/provenance-io/provenance/x/attribute/keeper" attributetypes "github.com/provenance-io/provenance/x/attribute/types" "github.com/provenance-io/provenance/x/exchange" "github.com/provenance-io/provenance/x/hold" ibchookstypes "github.com/provenance-io/provenance/x/ibchooks/types" + "github.com/provenance-io/provenance/x/marker/types" msgfeetypes "github.com/provenance-io/provenance/x/msgfees/types" oracletypes "github.com/provenance-io/provenance/x/oracle/types" triggertypes "github.com/provenance-io/provenance/x/trigger/types" @@ -120,6 +124,19 @@ 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 + Handler: func(ctx sdk.Context, app *App, vm module.VersionMap) (module.VersionMap, error) { + var err error + vm, err = runModuleMigrations(ctx, app, vm) + if err != nil { + return nil, err + } + + updateIbcMarkerDenomMetadata(ctx, app) + + return vm, nil + }, + }, "saffron": { // upgrade for v1.17.0, Handler: func(ctx sdk.Context, app *App, vm module.VersionMap) (module.VersionMap, error) { var err error @@ -135,6 +152,7 @@ var upgrades = map[string]appUpgrade{ setupICQ(ctx, app) updateMaxSupply(ctx, app) setExchangeParams(ctx, app) + updateIbcMarkerDenomMetadata(ctx, app) return vm, nil }, @@ -360,3 +378,41 @@ func setExchangeParams(ctx sdk.Context, app *App) { } ctx.Logger().Info("Done ensuring exchange module params are set.") } + +// updateIbcMarkerDenomMetadata iterates markers and creates denom metadata for ibc markers +// TODO: Remove with the saffron handlers. +func updateIbcMarkerDenomMetadata(ctx sdk.Context, app *App) { + ctx.Logger().Info("Updating ibc marker denom metadata") + app.MarkerKeeper.IterateMarkers(ctx, func(record types.MarkerAccountI) bool { + if !strings.HasPrefix(record.GetDenom(), "ibc/") { + return false + } + + hash, err := transfertypes.ParseHexHash(strings.TrimPrefix(record.GetDenom(), "ibc/")) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("invalid denom trace hash: %s, error: %s", hash.String(), err)) + return false + } + denomTrace, found := app.TransferKeeper.GetDenomTrace(ctx, hash) + if !found { + ctx.Logger().Error(fmt.Sprintf("trace not found: %s, error: %s", hash.String(), err)) + return false + } + + parts := strings.Split(denomTrace.Path, "/") + if len(parts) == 2 && parts[0] == "transfer" { + ctx.Logger().Info(fmt.Sprintf("Adding metadata to %s", record.GetDenom())) + chainID := app.Ics20MarkerHooks.GetChainID(ctx, parts[0], parts[1], app.IBCKeeper) + markerMetadata := banktypes.Metadata{ + Base: record.GetDenom(), + Name: chainID + "/" + denomTrace.BaseDenom, + Display: chainID + "/" + denomTrace.BaseDenom, + Description: denomTrace.BaseDenom + " from " + chainID, + } + app.BankKeeper.SetDenomMetaData(ctx, markerMetadata) + } + + return false + }) + ctx.Logger().Info("Done updating ibc marker denom metadata") +} diff --git a/app/upgrades_test.go b/app/upgrades_test.go index f264b24dee..1a7aa2a0bc 100644 --- a/app/upgrades_test.go +++ b/app/upgrades_test.go @@ -433,6 +433,18 @@ func (s *UpgradeTestSuite) TestSaffronRC1() { s.AssertUpgradeHandlerLogs("saffron-rc1", expInLog, nil) } +func (s *UpgradeTestSuite) TestSaffronRC2() { + // Each part is (hopefully) tested thoroughly on its own. + // So for this test, just make sure there's log entries for each part being done. + + expInLog := []string{ + "INF Updating ibc marker denom metadata", + "INF Done updating ibc marker denom metadata", + } + + s.AssertUpgradeHandlerLogs("saffron-rc2", expInLog, nil) +} + func (s *UpgradeTestSuite) TestSaffron() { // Each part is (hopefully) tested thoroughly on its own. // So for this test, just make sure there's log entries for each part being done. @@ -445,6 +457,8 @@ func (s *UpgradeTestSuite) TestSaffron() { "INF Updating MaxSupply marker param", "INF Done updating MaxSupply marker param", "INF Ensuring exchange module params are set.", + "INF Updating ibc marker denom metadata", + "INF Done updating ibc marker denom metadata", } s.AssertUpgradeHandlerLogs("saffron", expInLog, nil) diff --git a/docker/blockchain/Dockerfile b/docker/blockchain/Dockerfile index 6fa632a18d..832d56cb5d 100644 --- a/docker/blockchain/Dockerfile +++ b/docker/blockchain/Dockerfile @@ -1,6 +1,5 @@ FROM golang:1.20-bullseye as build ARG VERSION -ARG ARCH=x86_64 WORKDIR /go/src/github.com/provenance-io/provenance @@ -23,21 +22,35 @@ COPY Makefile sims.mk ./ # Build and install provenanced ENV VERSION=$VERSION -RUN make VERSION=${VERSION} WITH_CLEVELDB=true install +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then \ + echo "Unsupported architecture (required: x86_64 or aarch64): $ARCH"; \ + exit 1; \ + fi && \ + echo "Building and installing provenance for Arch: $ARCH"; \ + make VERSION=${VERSION} install ### FROM debian:bullseye-slim as run -ARG ARCH=x86_64 ENV LD_LIBRARY_PATH="/usr/local/lib" RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y curl jq libleveldb-dev && \ apt-get clean && \ rm -rf /var/lib/apt/lists/ + -COPY --from=build /go/src/github.com/provenance-io/provenance/vendor/github.com/CosmWasm/wasmvm/internal/api/libwasmvm.$ARCH.so /usr/local/lib +COPY --from=build /go/src/github.com/provenance-io/provenance/vendor/github.com/CosmWasm/wasmvm/internal/api/libwasmvm.*.so /tmp COPY --from=build /go/bin/provenanced /usr/bin/provenanced +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then \ + echo "Unsupported architecture (required: x86_64 or aarch64): $ARCH"; \ + exit 1; \ + fi && \ + cp /tmp/libwasmvm.$ARCH.so /usr/local/lib/. && \ + rm /tmp/libwasmvm.*.so + ENV PIO_HOME=/home/provenance WORKDIR /home/provenance 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%. diff --git a/x/ibchooks/marker_hooks.go b/x/ibchooks/marker_hooks.go index 7dd9fb33af..73f86e61e9 100644 --- a/x/ibchooks/marker_hooks.go +++ b/x/ibchooks/marker_hooks.go @@ -114,20 +114,20 @@ func (h MarkerHooks) getExistingSupply(ctx sdktypes.Context, marker *markertypes // addDenomMetaData adds denom metadata for ibc token func (h MarkerHooks) addDenomMetaData(ctx sdktypes.Context, packet exported.PacketI, ibcKeeper *ibckeeper.Keeper, ibcDenom string, data transfertypes.FungibleTokenPacketData) error { - chainID := h.GetChainID(ctx, packet, ibcKeeper) + chainID := h.GetChainID(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), ibcKeeper) markerMetadata := banktypes.Metadata{ Base: ibcDenom, Name: chainID + "/" + data.Denom, Display: chainID + "/" + data.Denom, - Description: data.Denom + " from chain " + chainID, + Description: data.Denom + " from " + chainID, } return h.MarkerKeeper.SetDenomMetaData(ctx, markerMetadata, authtypes.NewModuleAddress(types.ModuleName)) } -// GetChainID returns the source chain id from packet for `07-tendermint` client connection or returns `unknown` -func (h MarkerHooks) GetChainID(ctx sdktypes.Context, packet exported.PacketI, ibcKeeper *ibckeeper.Keeper) string { +// GetChainID returns the source chain id from packet for a `07-tendermint` client connection or returns `unknown` +func (h MarkerHooks) GetChainID(ctx sdktypes.Context, sourcePort, sourceChannel string, ibcKeeper *ibckeeper.Keeper) string { chainID := "unknown" - channel, found := ibcKeeper.ChannelKeeper.GetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel()) + channel, found := ibcKeeper.ChannelKeeper.GetChannel(ctx, sourcePort, sourceChannel) if !found { return chainID } @@ -139,11 +139,9 @@ func (h MarkerHooks) GetChainID(ctx sdktypes.Context, packet exported.PacketI, i if !found { return chainID } - if clientState.ClientType() == "07-tendermint" { - tmClientState, ok := clientState.(*tendermintclient.ClientState) - if ok { - chainID = tmClientState.ChainId - } + tmClientState, ok := clientState.(*tendermintclient.ClientState) + if ok { + return tmClientState.ChainId } return chainID } diff --git a/x/ibchooks/marker_hooks_test.go b/x/ibchooks/marker_hooks_test.go index f991f353cb..70a6b61eb8 100644 --- a/x/ibchooks/marker_hooks_test.go +++ b/x/ibchooks/marker_hooks_test.go @@ -160,7 +160,7 @@ func (suite *MarkerHooksTestSuite) TestAddUpdateMarker() { assert.Equal(t, marker.GetDenom(), metadata.Base, "Metadata Base should equal marker denom") assert.Equal(t, "testchain2/"+tc.denom, metadata.Name, "Metadata Name should be chainid/denom") assert.Equal(t, "testchain2/"+tc.denom, metadata.Display, "Metadata Display should be chainid/denom") - assert.Equal(t, tc.denom+" from chain testchain2", metadata.Description, "Metadata Description is incorrect") + assert.Equal(t, tc.denom+" from testchain2", metadata.Description, "Metadata Description is incorrect") assert.Len(t, marker.GetAccessList(), len(tc.expTransAuths), "Resulting access list does not equal expect length") for _, access := range marker.GetAccessList() { assert.Len(t, access.GetAccessList(), 1, "Expecting permissions list to only one item") diff --git a/x/oracle/spec/README.md b/x/oracle/spec/README.md index 46e222a1a5..930056186d 100644 --- a/x/oracle/spec/README.md +++ b/x/oracle/spec/README.md @@ -5,6 +5,9 @@ The oracle module provides the Provenance Blockchain with the capability to dyna One challenge that the Provenance Blockchain faces is supporting each Provenance Blockchain Zone with a unique set of queries. It is not feasible to create an evolving set of queries for each chain. Furthermore, it is not desirable for other parties to request Provenance to build these endpoints for them and then upgrade. This module resolves these issues by enabling Provenance Blockchain zones to manage their own oracle. +## Acknowledgements +We appreciate the substantial contributions made by Strangelove Ventures and Quasar Finance through their work on the [Async ICQ Module](https://github.com/strangelove-ventures/async-icq) and [Interchain Query Demo](https://github.com/quasar-finance/interchain-query-demo). These resources were of paramount importance in informing the development of our oracle module. + ## Contents 1. **[Concepts](01_concepts.md)** 2. **[State](02_state.md)**