Repository: osmosis-labs/osmosis
Pull request #3683 (2022-12-10 01:05:48)

ProtoRev: Route building
## What is the purpose of the change

This PR implements the logic of building cyclic arbitrage routes given a swap. There are two primary methods for route generation: Highest Liquidity Pools and Hot Routes.


## Brief Changelog

- *Build routes that start and end with osmo (Highest Liquidity Pools)*
- *Build routes that start and end with atom (Highest Liquidity Pools)*
- *Build routes that start and end with osmo or atom from the set of hot routes (Hot Routes)*


## Testing and Verifying

This change added tests and can be verified as follows:

- *Added unit tests for atom routes*
- *Added unit tests for osmo routes*
- *Added unit tests for routes built with tokenPairArbRoutes*
- *Added unit tests for BuildRoutes (which is a wrapper to the above three)*

## Documentation and Release Note

### Highest Liquidity Pools

There are two types of trades that must be considered:

1. **The assets in the trade are neither Atom nor Osmo**
    - In this case, `x/protorev` will sandwich the pool with either Osmo or Atom on the other end. For example, say that a swap occurs on the **Akash** ↔ **Juno** pool. There are four possible routes that can be taken (in order of trades made starting on the left and ending on the right).
        - (Osmo → Akash), (Akash → Juno), (Juno → Osmo)
        - (Osmo → Juno), (Juno → Akash), (Akash → Osmo)
        - (Atom → Akash), (Akash → Juno), Juno → Atom)
        - (Atom → Juno), (Juno → Akash), (Akash → Atom)
    - Capturing cyclic arbitrage opportunities happen in the opposite direction of the trade. Using the example above, we can cut down the routes to only two routes if we know that the user traded Akash → Juno. The routes in that case would be
        - (Osmo → Juno), (Juno → Akash), (Akash → Osmo)
        - (Atom → Juno), (Juno → Akash), (Akash → Atom)
2. **The assets in the trade include either Atom or Osmo or both**
- If the **************Osmo ↔ Atom**************  pool is swapped against, then the highest liquidity pool route building method does not produce any routes. Otherwise, if only one of OSMO or ATOM was swapped against in a single pool, then `x/protorev` will look for the opposite asset pools from what was traded in the pool. For example, say a swap has been executed on the  ************Osmo ↔ TokenXYZ************ pool, tendering OSMO and receiving TokenXYZ, the route generated would be:
    - (Atom → TokenXYZ), (TokenXYZ → Osmo), (Osmo → Atom)

In both cases, the route that is built will always sandwich the swap that was made. However, we allow for more flexibility in route generation as the highest liquidity method may not be optimal via Hot Routes.

### Hot Routes
Populated through the admin account (which will be implemented in a later PR), the module’s keeper holds a KV store that associates token pairs (for example, osmo/juno) to the routes that result in a high percentage of arbitrage profit on Osmosis (as determined by external analysis).

The purpose of storing Hot Routes is a recognition that the Highest Liquidity Pool method may not present the best arbitrage routes. As such, hot routes can be configured through governance to store additional routes that may be more effective at capturing arbitrage opportunities. Each hot route will store a placeholder (the value 0) for where the current swapped pool will fit into the trade. TokenPairArbRoutes are always going to be sorted in the direction of the trade i.e. if we see a swap of ***Atom -> Osmo*** we can immediately find a route that goes in the opposite direction by replacing the placeholder (0) with this pool id.



Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c9b2d77f6..259f57832 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,20 +26,32 @@ jobs:
     name: osmosis ${{ matrix.arch }} for ${{ matrix.targetos }}
     steps:
       - uses: actions/checkout@v2
+      - name: Get git diff
+        uses: technote-space/get-diff-action@v6.0.1
+        with:
+          PATTERNS: |
+            **/**.wasm
+            **/**!(test).go
+            go.mod
+            go.sum
+            Makefile
       - uses: actions/setup-go@v2.1.4
         with:
           go-version: '^1.18'
         env:
           GOOS: ${{ matrix.targetos }}
           GOARCH: ${{ matrix.arch }}
+        if: env.GIT_DIFF
 
       - name: Compile osmosis
         run: |
           go mod download
           cd cmd/osmosisd
           go build .
+        if: env.GIT_DIFF
 
       - uses: actions/upload-artifact@v2
         with:
           name: osmosisd ${{ matrix.targetos }} ${{ matrix.arch }}
           path: cmd/osmosisd/osmosisd
+        if: env.GIT_DIFF
diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml
index 6630f31b9..e785b0996 100644
--- a/.github/workflows/contracts.yml
+++ b/.github/workflows/contracts.yml
@@ -32,16 +32,19 @@ jobs:
         uses: dtolnay/rust-toolchain@1.65.0
         with:
           target: wasm32-unknown-unknown
+        if: env.GIT_DIFF
 
       - name: Add the wasm target
         working-directory: ${{ matrix.contract.workdir }}
         run: >
           rustup target add wasm32-unknown-unknown;
+        if: env.GIT_DIFF
 
       - name: Test
         working-directory: ${{ matrix.contract.workdir }}
         run: >
           cargo test
+        if: env.GIT_DIFF
 
 #      - name: Set latest cw-optimizoor version
 #        run: >
@@ -70,6 +73,7 @@ jobs:
             --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
             --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
             cosmwasm/workspace-optimizer:0.12.10
+        if: env.GIT_DIFF
 
       - name: 'Upload optimized contract artifact'
         uses: actions/upload-artifact@v3
@@ -77,6 +81,7 @@ jobs:
           name: ${{ matrix.contract.name }}
           path: ${{ matrix.contract.workdir }}${{ matrix.contract.build }}
           retention-days: 1
+        if: env.GIT_DIFF
 
       - name: 'Upload Cargo.lock artifact'
         uses: actions/upload-artifact@v3
@@ -84,12 +89,14 @@ jobs:
           name: Cargo.lock
           path: ${{ matrix.contract.workdir }}Cargo.lock
           retention-days: 1
+        if: env.GIT_DIFF
 
       - name: Check Test Data
         working-directory: ${{ matrix.contract.workdir }}
         if: ${{ matrix.contract.output != null }}
         run: >
           diff ${{ matrix.contract.output }} ${{ matrix.contract.build }}
+        if: env.GIT_DIFF
   
 
   lints:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2debe754e..108693b77 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -21,6 +21,8 @@ jobs:
             **/**.go
             go.mod
             go.sum
+            .github/**
+            Makefile
       - name: Get data from build cache
         uses: actions/cache@v2
         with:
@@ -39,6 +41,7 @@ jobs:
           key: ${{ runner.os }}-go-linter-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-linter-${{ matrix.go-version }}-
+        if: env.GIT_DIFF
       - name: Run golangci-lint
         run: make lint
         if: env.GIT_DIFF
@@ -53,5 +56,13 @@ jobs:
           # Full git history is needed to get a proper list of changed files
           # within `super-linter`.
           fetch-depth: 0
+      - uses: technote-space/get-diff-action@v6.0.1
+        with:
+          PATTERNS: |
+            **/**.md
+            go.mod
+            go.sum
+            Makefile
       - name: Run documentation linter
         run: make mdlint
+        if: env.GIT_DIFF
diff --git a/.github/workflows/sim.yml b/.github/workflows/sim.yml
index 78df7c18c..d7c1143f3 100644
--- a/.github/workflows/sim.yml
+++ b/.github/workflows/sim.yml
@@ -14,11 +14,23 @@ jobs:
       - uses: actions/setup-go@v2.1.4
         with:
           go-version: 1.18
-      - name: Display go version
-        run: go version
+      -
+        name: Get git diff
+        uses: technote-space/get-diff-action@v6.0.1
+        with:
+          PATTERNS: |
+            **/**.wasm
+            **/**!(test).go
+            tests/**/**.go
+            go.mod
+            go.sum
+            Makefile
       - name: Run full application simulation
         run: |
           make test-sim-app
+        if: env.GIT_DIFF
+
       - name: Run simulation determinism check
         run: |
           make test-sim-determinism
+        if: env.GIT_DIFF
diff --git a/.gitignore b/.gitignore
index 705d20fc0..f9b128d09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -230,3 +230,7 @@ blocks.db
 # Ignore e2e test artifacts (which clould leak information if commited)
 .ash_history
 .bash_history
+
+# Python
+**/venv/*
+**/*.pyc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2db6b4efb..2c23240a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * [#3609](https://github.com/osmosis-labs/osmosis/pull/3609) Add Downtime-detection module.
 * [#2788](https://github.com/osmosis-labs/osmosis/pull/2788) Add logarithm base 2 implementation.
 * [#3677](https://github.com/osmosis-labs/osmosis/pull/3677) Add methods for cloning and mutative multiplication on osmomath.BigDec.
+* [#3676](https://github.com/osmosis-labs/osmosis/pull/3676) implement `PowerInteger` function on `osmomath.BigDec` 
+* [#3678](https://github.com/osmosis-labs/osmosis/pull/3678) implement mutative `PowerIntegerMut` function on `osmomath.BigDec`.
+* [#3693](https://github.com/osmosis-labs/osmosis/pull/3693) Add `EstimateSwapExactAmountOut` query to stargate whitelist
 
 ### Bug fixes
 
@@ -54,7 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Misc Improvements
 
-* [#3611](https://github.com/osmosis-labs/osmosis/pull/3611) Introduce osmocli, to automate thousands of lines of CLI boilerplate
+* [#3611](https://github.com/osmosis-labs/osmosis/pull/3611),[#3647](https://github.com/osmosis-labs/osmosis/pull/3647) Introduce osmocli, to automate thousands of lines of CLI boilerplate
 * [#3634](https://github.com/osmosis-labs/osmosis/pull/3634) (Makefile) Ensure correct golang version in make build and make install. (Thank you @jhernandezb )
 
 ## v13.0.0
@@ -89,7 +92,6 @@ This release includes stableswap, and expands the IBC safety & composability fun
   - This PR adds v2 queries for doing the correct thing, and giving people time to migrate from v1beta1 queries to v2.
   - It also changes cosmwasm to only allow the v2 queries, as no contracts on Osmosis mainnet uses the v1beta1 queries.
 
-
 ### Bug fixes
 
 * [#2803](https://github.com/osmosis-labs/osmosis/pull/2803) Fix total pool liquidity CLI query.
diff --git a/app/app.go b/app/app.go
index 5964ee19e..f2121ab2c 100644
--- a/app/app.go
+++ b/app/app.go
@@ -74,6 +74,7 @@ var (
 	// module accounts that are allowed to receive tokens.
 	allowedReceivingModAcc = map[string]bool{ibc_hooks.WasmHookModuleAccountAddr.String(): true}
 
+	// TODO: Refactor wasm items into a wasm.go file
 	// WasmProposalsEnabled enables all x/wasm proposals when it's value is "true"
 	// and EnableSpecificWasmProposals is empty. Otherwise, all x/wasm proposals
 	// are disabled.
diff --git a/chain.schema.json b/chain.schema.json
index a0114f753..d337bbba3 100644
--- a/chain.schema.json
+++ b/chain.schema.json
@@ -2,21 +2,21 @@
   "$schema": "http://json-schema.org/draft-07/schema#",
   "codebase":{
     "git_repo": "https://github.com/osmosis-labs/osmosis",
-    "recommended_version": "v12.3.0",
+    "recommended_version": "v13.1.0",
     "compatible_versions": [
-      "v12.3.0",
-      "v12.2.0",
-      "v12.2.1"
+      "v13.1.0",
+      "v13.1.1",
+      "v13.1.2"
     ],
     "binaries": {
-      "linux/amd64": "https://github.com/osmosis-labs/osmosis/releases/download/v12.3.0/osmosisd-12.3.0-linux-amd64?checksum=sha256:958210c919d13c281896fa9773c323c5534f0fa46d74807154f737609a00db70",
-      "linux/arm64": "https://github.com/osmosis-labs/osmosis/releases/download/v12.3.0/osmosisd-12.3.0-linux-arm64?checksum=sha256:a931618c8a839c30e5cecfd2a88055cda1d68cc68557fe3303fe14e2de3bef8f"
+      "linux/amd64": "https://github.com/osmosis-labs/osmosis/releases/download/v13.1.0/osmosisd-13.1.0-linux-amd64?checksum=sha256:50e734ee44e15c00927e4ce6c721bd79f5580f2b70cf37df52329224ad8089f0",
+      "linux/arm64": "https://github.com/osmosis-labs/osmosis/releases/download/v13.1.0/osmosisd-13.1.0-linux-arm64?checksum=sha256:ddc45deb04e7f4b7bdaaf3263ff1cbc469f7bfc51d3c9dae5d6817cb9e3c0bd7"
     },
     "cosmos_sdk_version": "0.45",
     "tendermint_version": "0.34",
-    "cosmwasm_version": "0.28",
+    "cosmwasm_version": "0.29",
     "cosmwasm_enabled": true,
-    "ibc_go_version": "3.3.0",
+    "ibc_go_version": "3.4.0",
     "ics_enabled": [
       "ics20-1"
     ],
@@ -64,7 +64,13 @@
       {
         "name": "v12",
         "tag" : "v12.3.0",
-        "height" : 6246000
+        "height" : 6246000,
+        "next_version_name" : "v13"
+      },
+      {
+        "name": "v13",
+        "tag" : "v13.1.0",
+        "height" : 7241500
       }
     ]
   }
diff --git a/osmomath/decimal.go b/osmomath/decimal.go
index 55dbd399f..cba13ca44 100644
--- a/osmomath/decimal.go
+++ b/osmomath/decimal.go
@@ -989,3 +989,31 @@ func (x BigDec) CustomBaseLog(base BigDec) BigDec {
 
 	return y
 }
+
+// PowerInteger takes a given decimal to an integer power
+// and returns the result. Non-mutative. Uses square and multiply
+// algorithm for performing the calculation.
+func (d BigDec) PowerInteger(power uint64) BigDec {
+	clone := d.Clone()
+	return clone.PowerIntegerMut(power)
+}
+
+// PowerIntegerMut takes a given decimal to an integer power
+// and returns the result. Mutative. Uses square and multiply
+// algorithm for performing the calculation.
+func (d BigDec) PowerIntegerMut(power uint64) BigDec {
+	if power == 0 {
+		return OneDec()
+	}
+	tmp := OneDec()
+
+	for i := power; i > 1; {
+		if i%2 != 0 {
+			tmp = tmp.MulMut(d)
+		}
+		i /= 2
+		d = d.MulMut(d)
+	}
+
+	return d.MulMut(tmp)
+}
diff --git a/osmomath/math.go b/osmomath/math.go
index d8f341bb6..0e07cac82 100644
--- a/osmomath/math.go
+++ b/osmomath/math.go
@@ -18,6 +18,10 @@ var (
 	one_half sdk.Dec = sdk.MustNewDecFromStr("0.5")
 	one      sdk.Dec = sdk.OneDec()
 	two      sdk.Dec = sdk.MustNewDecFromStr("2")
+
+	// https://www.wolframalpha.com/input?i=2.718281828459045235360287471352662498&assumption=%22ClashPrefs%22+-%3E+%7B%22Math%22%7D
+	// nolint: unused
+	eulersNumber = MustNewDecFromStr("2.718281828459045235360287471352662498")
 )
 
 // Returns the internal "power precision".
diff --git a/osmoutils/cli_helpers.go b/osmoutils/cli_helpers.go
index 3de665de0..c9e4db9d2 100644
--- a/osmoutils/cli_helpers.go
+++ b/osmoutils/cli_helpers.go
@@ -8,6 +8,7 @@ import (
 	"github.com/cosmos/cosmos-sdk/client/flags"
 	"github.com/cosmos/cosmos-sdk/testutil/network"
 	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/tendermint/tendermint/crypto/ed25519"
 )
 
 func DefaultFeeString(cfg network.Config) string {
@@ -72,3 +73,14 @@ func ParseSdkValAddressFromString(s string, separator string) []sdk.ValAddress {
 
 	return parsedAddr
 }
+
+// CreateRandomAccounts is a function return a list of randomly generated AccAddresses
+func CreateRandomAccounts(numAccts int) []sdk.AccAddress {
+	testAddrs := make([]sdk.AccAddress, numAccts)
+	for i := 0; i < numAccts; i++ {
+		pk := ed25519.GenPrivKey().PubKey()
+		testAddrs[i] = sdk.AccAddress(pk.Address())
+	}
+
+	return testAddrs
+}
diff --git a/osmoutils/osmocli/query_cmd_wrap.go b/osmoutils/osmocli/query_cmd_wrap.go
index 5515868b1..b1327826c 100644
--- a/osmoutils/osmocli/query_cmd_wrap.go
+++ b/osmoutils/osmocli/query_cmd_wrap.go
@@ -11,13 +11,12 @@ import (
 	grpc1 "github.com/gogo/protobuf/grpc"
 	"github.com/gogo/protobuf/proto"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 )
 
-func QueryIndexCmd(moduleName string) *cobra.Command {
-	cmd := IndexCmd(moduleName)
-	cmd.Short = fmt.Sprintf("Querying commands for the %s module", moduleName)
-	return cmd
-}
+// global variable set on index command.
+// helps populate Longs, when not set in QueryDescriptor.
+var lastQueryModuleName string
 
 type QueryDescriptor struct {
 	Use   string
@@ -33,25 +32,70 @@ type QueryDescriptor struct {
 	CustomFlagOverrides map[string]string
 	// Map of FieldName -> CustomParseFn
 	CustomFieldParsers map[string]CustomFieldParserFn
+
+	ParseQuery func(args []string, flags *pflag.FlagSet) (proto.Message, error)
+
+	ModuleName string
+	numArgs    int
 }
 
-func SimpleQueryFromDescriptor[reqP proto.Message, querier any](desc QueryDescriptor, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
-	numArgs := ParseNumFields[reqP]() - len(desc.CustomFlagOverrides)
+func QueryIndexCmd(moduleName string) *cobra.Command {
+	cmd := IndexCmd(moduleName)
+	cmd.Short = fmt.Sprintf("Querying commands for the %s module", moduleName)
+	lastQueryModuleName = moduleName
+	return cmd
+}
+
+func AddQueryCmd[Q proto.Message, querier any](cmd *cobra.Command, newQueryClientFn func(grpc1.ClientConn) querier, f func() (*QueryDescriptor, Q)) {
+	desc, _ := f()
+	prepareDescriptor[Q](desc)
+	subCmd := BuildQueryCli[Q](desc, newQueryClientFn)
+	cmd.AddCommand(subCmd)
+}
+
+func (desc *QueryDescriptor) FormatLong(moduleName string) {
+	desc.Long = FormatLongDesc(desc.Long, NewLongMetadata(moduleName).WithShort(desc.Short))
+}
+
+func prepareDescriptor[reqP proto.Message](desc *QueryDescriptor) {
+	if !desc.HasPagination {
+		desc.HasPagination = ParseHasPagination[reqP]()
+	}
+	if desc.QueryFnName == "" {
+		desc.QueryFnName = ParseExpectedQueryFnName[reqP]()
+	}
+	if strings.Contains(desc.Long, "{") {
+		if desc.ModuleName == "" {
+			desc.ModuleName = lastQueryModuleName
+		}
+		desc.FormatLong(desc.ModuleName)
+	}
+
+	desc.numArgs = ParseNumFields[reqP]() - len(desc.CustomFlagOverrides)
 	if desc.HasPagination {
-		numArgs = numArgs - 1
+		desc.numArgs = desc.numArgs - 1
+	}
+}
+
+func BuildQueryCli[reqP proto.Message, querier any](desc *QueryDescriptor, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
+	prepareDescriptor[reqP](desc)
+	if desc.ParseQuery == nil {
+		desc.ParseQuery = func(args []string, fs *pflag.FlagSet) (proto.Message, error) {
+			flagAdvice := FlagAdvice{
+				HasPagination:       desc.HasPagination,
+				CustomFlagOverrides: desc.CustomFlagOverrides,
+				CustomFieldParsers:  desc.CustomFieldParsers,
+			}.Sanitize()
+			return ParseFieldsFromFlagsAndArgs[reqP](flagAdvice, fs, args)
+		}
 	}
-	flagAdvice := FlagAdvice{
-		HasPagination:       desc.HasPagination,
-		CustomFlagOverrides: desc.CustomFlagOverrides,
-		CustomFieldParsers:  desc.CustomFieldParsers,
-	}.Sanitize()
+
 	cmd := &cobra.Command{
 		Use:   desc.Use,
 		Short: desc.Short,
 		Long:  desc.Long,
-		Args:  cobra.ExactArgs(numArgs),
-		RunE: NewQueryLogicAllFieldsAsArgs[reqP](
-			flagAdvice, desc.QueryFnName, newQueryClientFn),
+		Args:  cobra.ExactArgs(desc.numArgs),
+		RunE:  queryLogic(desc, newQueryClientFn),
 	}
 	flags.AddQueryFlagsToCmd(cmd)
 	AddFlags(cmd, desc.Flags)
@@ -70,25 +114,23 @@ func SimpleQueryFromDescriptor[reqP proto.Message, querier any](desc QueryDescri
 func SimpleQueryCmd[reqP proto.Message, querier any](use string, short string, long string,
 	moduleName string, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
 	desc := QueryDescriptor{
-		Use:           use,
-		Short:         short,
-		Long:          FormatLongDesc(long, NewLongMetadata(moduleName).WithShort(short)),
-		HasPagination: ParseHasPagination[reqP](),
-		QueryFnName:   ParseExpectedQueryFnName[reqP](),
+		Use:   use,
+		Short: short,
+		Long:  FormatLongDesc(long, NewLongMetadata(moduleName).WithShort(short)),
 	}
-	return SimpleQueryFromDescriptor[reqP](desc, newQueryClientFn)
+	return BuildQueryCli[reqP](&desc, newQueryClientFn)
 }
 
 func GetParams[reqP proto.Message, querier any](moduleName string,
 	newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
-	return SimpleQueryFromDescriptor[reqP](QueryDescriptor{
+	return BuildQueryCli[reqP](&QueryDescriptor{
 		Use:         "params [flags]",
 		Short:       fmt.Sprintf("Get the params for the x/%s module", moduleName),
 		QueryFnName: "Params",
 	}, newQueryClientFn)
 }
 
-func callQueryClientFn[reqP proto.Message, querier any](ctx context.Context, fnName string, req reqP, q querier) (res proto.Message, err error) {
+func callQueryClientFn(ctx context.Context, fnName string, req proto.Message, q any) (res proto.Message, err error) {
 	qVal := reflect.ValueOf(q)
 	method := qVal.MethodByName(fnName)
 	args := []reflect.Value{
@@ -109,7 +151,7 @@ func callQueryClientFn[reqP proto.Message, querier any](ctx context.Context, fnN
 	return res, nil
 }
 
-func NewQueryLogicAllFieldsAsArgs[reqP proto.Message, querier any](flagAdvice FlagAdvice, keeperFnName string,
+func queryLogic[querier any](desc *QueryDescriptor,
 	newQueryClientFn func(grpc1.ClientConn) querier) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		clientCtx, err := client.GetClientQueryContext(cmd)
@@ -117,14 +159,13 @@ func NewQueryLogicAllFieldsAsArgs[reqP proto.Message, querier any](flagAdvice Fl
 			return err
 		}
 		queryClient := newQueryClientFn(clientCtx)
-		var req reqP
 
-		req, err = ParseFieldsFromFlagsAndArgs[reqP](flagAdvice, cmd.Flags(), args)
+		req, err := desc.ParseQuery(args, cmd.Flags())
 		if err != nil {
 			return err
 		}
 
-		res, err := callQueryClientFn(cmd.Context(), keeperFnName, req, queryClient)
+		res, err := callQueryClientFn(cmd.Context(), desc.QueryFnName, req, queryClient)
 		if err != nil {
 			return err
 		}
diff --git a/osmoutils/osmocli/tx_cmd_wrap.go b/osmoutils/osmocli/tx_cmd_wrap.go
index 7c2096cb2..48725fe9e 100644
--- a/osmoutils/osmocli/tx_cmd_wrap.go
+++ b/osmoutils/osmocli/tx_cmd_wrap.go
@@ -36,21 +36,29 @@ type TxCliDesc struct {
 	CustomFieldParsers map[string]CustomFieldParserFn
 }
 
+func AddTxCmd[M sdk.Msg](cmd *cobra.Command, f func() (*TxCliDesc, M)) {
+	desc, _ := f()
+	subCmd := BuildTxCli[M](desc)
+	cmd.AddCommand(subCmd)
+}
+
 func BuildTxCli[M sdk.Msg](desc *TxCliDesc) *cobra.Command {
 	desc.TxSignerFieldName = strings.ToLower(desc.TxSignerFieldName)
 	if desc.NumArgs == 0 {
 		// NumArgs = NumFields - 1, since 1 field is from the msg
 		desc.NumArgs = ParseNumFields[M]() - 1 - len(desc.CustomFlagOverrides) - len(desc.CustomFieldParsers)
 	}
-	desc.ParseAndBuildMsg = func(clientCtx client.Context, args []string, flags *pflag.FlagSet) (sdk.Msg, error) {
-		flagAdvice := FlagAdvice{
-			IsTx:                true,
-			TxSenderFieldName:   desc.TxSignerFieldName,
-			FromValue:           clientCtx.GetFromAddress().String(),
-			CustomFlagOverrides: desc.CustomFlagOverrides,
-			CustomFieldParsers:  desc.CustomFieldParsers,
-		}.Sanitize()
-		return ParseFieldsFromFlagsAndArgs[M](flagAdvice, flags, args)
+	if desc.ParseAndBuildMsg == nil {
+		desc.ParseAndBuildMsg = func(clientCtx client.Context, args []string, flags *pflag.FlagSet) (sdk.Msg, error) {
+			flagAdvice := FlagAdvice{
+				IsTx:                true,
+				TxSenderFieldName:   desc.TxSignerFieldName,
+				FromValue:           clientCtx.GetFromAddress().String(),
+				CustomFlagOverrides: desc.CustomFlagOverrides,
+				CustomFieldParsers:  desc.CustomFieldParsers,
+			}.Sanitize()
+			return ParseFieldsFromFlagsAndArgs[M](flagAdvice, flags, args)
+		}
 	}
 	return desc.BuildCommandCustomFn()
 }
diff --git a/scripts/approximations/README.md b/scripts/approximations/README.md
new file mode 100644
index 000000000..ebadb8819
--- /dev/null
+++ b/scripts/approximations/README.md
@@ -0,0 +1,65 @@
+# Math Operations Approximations
+
+## Context
+
+This is a set of scripts to approximate a mathematical operation using polynomial
+and rational approximations.
+
+The `main` script is in its respective function of `main.py`. It does the following:
+
+1. Computes polynomial and rational approximations of a given function (e^x by default), 
+returning the coefficients.
+
+1. Computes (x,y) coordinates for every kind of approximation given the same x coordinates.
+Plots the results for rough comparison.
+
+1. Plots the results for rough comparison.
+
+2. Computes the max error for every approximation given the same x coordinates.
+
+3. Computes and plots max errors for every approximation with a varying number of parameters.
+
+In other words, this script runs various approximation methods, plots their results and deltas
+from actual function values. It can be configured to print the maximum error.
+The exact behavior is controlled by the global variables at the top of `main.py`.
+
+The following are the resources used to create this:
+- <https://xn--2-umb.com/22/approximation>
+- <https://sites.tufts.edu/atasissa/files/2019/09/remez.pdf>
+
+In `main.py`, there is also an `exponent_approximation_choice` script.
+
+This is a shorter and simpler version of `main` that isolates the 13-parameter
+Chebyshev Rational approximation of e^x. We are planning to use it in production.
+Therefore, we need to perform coefficient truncations to 36 decimal points
+(the max `osmomath` supported precision). This truncation is applied
+to `exponent_approximation_choice` but not `main`.
+
+## Configuration
+
+Several parameters can be changed on the needs basis at the
+top of `main.py`.
+
+Some of the parameters include the function we are approximating, the [x_start, x_end] range of
+the approximation, and the number of terms to be used. For the full parameter list, see `main.py`.
+
+## Usage
+
+Assuming that you are in the root of the repository and have Sympy installed:
+
+```bash
+# Create a virtual environment.
+python3 -m venv ~/approx-venv
+
+# Start the environment
+source ~/approx-venv/bin/activate
+
+# Install dependencies in the virtual environment.
+pip install -r scripts/approximations/requirements.txt
+
+# Run the script.
+python3 scripts/approximations/main.py
+
+# Run tests
+python3 scripts/approximations/rational_test.py
+```
diff --git a/scripts/approximations/approximations.py b/scripts/approximations/approximations.py
new file mode 100644
index 000000000..198496980
--- /dev/null
+++ b/scripts/approximations/approximations.py
@@ -0,0 +1,191 @@
+import sympy as sp
+
+import rational
+import polynomial
+import chebyshev
+
+def linspace(start: sp.Float, end: sp.Float, num_points: int) -> list[sp.Float]:
+    """ Given [start, end] on the x-axis, creates the desired number of
+    equispaced points and returns them in increasing order as a list. 
+    """
+    if num_points == 1:
+        return [(end - start) / 2]
+
+    result: list[sp.Float] = []
+    for i in range(num_points):
+        cur_coord = sp.Float(start + i * (end - start) / (num_points - 1), 200)
+
+        if cur_coord is sp.nan:
+            raise ValueError("cur_coord is nan")
+
+        result.append(cur_coord)
+
+    return result
+
+def approx_and_eval_all(approximated_fn, num_parameters: int, x_coordinates) -> tuple:
+    """ Performs all supported approximations of the given function, evaluates
+    each wih the given x-coordinates.
+
+    Returns y-coordinates as a tuple in the following order:
+    - Evaluated Equispaced Polynomial
+    - Evaluated Chebyshev Polynomial
+    - Evaluated Chebyshev Rational
+    - True Y-coordinates as determined by Sympy's implementation of the function.
+    """
+    x_start = x_coordinates[0]
+    x_end = x_coordinates[-1]
+    
+    # Equispaced Polynomial Approximation
+    coefficients_equispaced_poly = equispaced_poly_approx(approximated_fn, x_start, x_end, num_parameters)
+    y_eqispaced_poly = polynomial.evaluate(x_coordinates, coefficients_equispaced_poly)
+
+    # Chebyshev Polynomial Approximation
+    coefficients_chebyshev_poly = chebyshev_poly_approx(approximated_fn, x_start, x_end, num_parameters)
+    y_chebyshev_poly = polynomial.evaluate(x_coordinates, coefficients_chebyshev_poly)
+
+    # Chebyshev Rational Approximation
+    numerator_coefficients_chebyshev_rational, denominator_coefficients_chebyshev_rational = chebyshev_rational_approx(approximated_fn, x_start, x_end, num_parameters)
+    y_chebyshev_rational = rational.evaluate(x_coordinates, numerator_coefficients_chebyshev_rational, denominator_coefficients_chebyshev_rational)
+
+    # Actual
+    y_actual = get_y_actual(approximated_fn, x_coordinates)
+
+    return (y_eqispaced_poly, y_chebyshev_poly, y_chebyshev_rational, y_actual)
+
+def get_y_actual(approximated_fn, x_coordinates) -> list[sp.Float]:
+    """ Given x coordinates and a function, returns a list of y coordinates, one for each x coordinate
+    that represent the true (x,y) coordinates of the function, as calculated by sp.
+    """
+    y_actual = []
+    for x in x_coordinates:
+        y_actual.append(approximated_fn(x))
+    return y_actual
+
+def compute_max_error(y_approximation, y_actual) -> sp.Float:
+    """ Given an approximated list of y values and actual y values, computes and returns
+    the maximum error delta between them.
+
+    CONTRACT:
+    - y_approximation and y_actual must be the same length
+    - for every i in range(len(y_approximation)), y_approximation[i] and y_actual[i] must correspond to the
+    same x coordinate
+    """
+    if len(y_approximation) != len(y_actual):
+        raise ValueError(F"y_approximation ({len(y_approximation)}) and y_actual ({len(y_actual)}) must be the same length.")
+
+    max: sp.Float = None
+    for i in range(len(y_approximation)):
+        cur_abs = sp.Abs(y_approximation[i] - y_actual[i])
+        if cur_abs is sp.nan:
+            raise ValueError(F"cur_abs is nan. y_approximation[i] ({y_approximation[i]}) and y_actual[i] ({y_actual[i]})")
+        if max is None:
+            max = cur_abs
+        else:
+            max = sp.Max(max, cur_abs)
+    return max
+
+def compute_error_range(y_approximation: list, y_actual: list) -> list[sp.Float]:
+    """ Given an approximated list of y values and actual y values, computes and returns
+    error deltas between them.
+
+    CONTRACT:
+    - y_approximation and y_actual must be the same length
+    - for every i in range(len(y_approximation)), y_approximation[i] and y_actual[i] must correspond to the
+    same x coordinate
+    """
+    result = []
+    for i in range(len(y_approximation)):
+        cur_abs = sp.Abs(y_approximation[i] - y_actual[i])
+        if cur_abs is sp.nan:
+            raise ValueError(F"cur_abs is nan. y_approximation[i] ({y_approximation[i]}) and y_actual[i] ({y_actual[i]})")
+        result.append(cur_abs)
+    return result
+
+def equispaced_poly_approx(fn, x_start: sp.Float, x_end: sp.Float, num_terms: int):
+    """ Returns the coefficients for an equispaced polynomial between x_start and x_end with num_terms terms.
+
+    The return value is a list of num_terms polynomial coefficients needed to get the returned y coordinates from returned x coordinates.
+    """
+    # Compute equispaced coordinates.
+    equispaced_nodes_x = linspace(x_start, x_end, num_terms)
+    y_nodes = sp.Matrix([fn(x) for x in equispaced_nodes_x])
+
+    # Construct a system of linear equations.
+    vandermonde_matrix = polynomial.construct_vandermonde_matrix(equispaced_nodes_x)
+
+    coef = vandermonde_matrix.solve(y_nodes)
+
+    return coef
+
+def chebyshev_poly_approx(fn, x_start: int, x_end: int, num_terms: int):
+    """ Returns the coefficients for a polynomial constructed from Chebyshev nodes between x_start and x_end with num_terms terms.
+
+    Equation for Chebyshev nodes:
+    x_i = (x_start + x_end)/2 + (x_end - x_start)/2 * cos((2*i + 1)/(2*num_terms) * pi)
+
+    The return value is a list of num_terms polynomial coefficients needed to get the returned y coordinates from returned x coordinates.
+    """
+    # Compute Chebyshev coordinates.
+    x_estimated, y_estimated = chebyshev.get_nodes(fn, x_start, x_end, num_terms)
+
+    # Construct a system of linear equations.
+    vandermonde_matrix = polynomial.construct_vandermonde_matrix(x_estimated)
+
+    # Solve the matrix to get the coefficients used in the final approximation polynomial.
+    coef = vandermonde_matrix.solve(sp.Matrix(y_estimated))
+
+    return coef
+
+def chebyshev_rational_approx(fn, x_start: int, x_end: int, num_parameters: int):
+    """ Returns a rational approximation between x_start and x_end with num_terms terms
+    using Chebyshev nodes.
+
+    Equation for Chebyshev nodes:
+    x_i = (x_start + x_end)/2 + (x_end - x_start)/2 * cos((2*i + 1)/(2*num_terms) * pi)
+
+    We approximate h(x) = p(x) / q(x)
+
+    Assume num_terms_numerator = 3.
+    Then, we have
+
+    h(x) = (p_0 + p_1 x + p_2 x^2) / (1 + q_1 x + q_2 x^2) 
+
+    The return value is a list with a 2 items where:
+    - item 1: num_terms equispaced x coordinates between x_start and x_end
+    - item 2: num_terms y coordinates for the equispaced x coordinates
+    """
+    if num_parameters % 2 == 0:
+        # if num_parameters is 6, we want (3, 4) terms
+        # assume h(x) = p(x) / q(x)
+        # we always set the first term of q(x) to 1 for ease of calculation.
+        # so, we want p(x) to have 3 terms and q(x) to have 4 terms.
+        num_terms_numerator = num_parameters // 2
+        num_terms_denominator = num_parameters // 2 + 1
+    else:
+        # if num_parameters is 5, we want (3, 3) terms
+        # assume h(x) = p(x) / q(x)
+        # we always set the first term of q(x) to 1 for ease of calculation.
+        # so, we want p(x) to have 3 terms and q(x) to have 4 terms.
+        num_terms_numerator = num_parameters // 2 + 1
+        num_terms_denominator = num_parameters // 2 + 1
+
+    # Compute Chebyshev coordinates.
+    x_chebyshev, y_chebyshev = chebyshev.get_nodes(fn, x_start, x_end, num_parameters)
+
+    # Construct a system of linear equations.
+    matrix = rational.construct_rational_eval_matrix(x_chebyshev, y_chebyshev, num_terms_numerator, num_terms_denominator)
+
+    # Solve the matrix to get the coefficients used in the final approximation polynomial.
+    # coef = np.linalg.solve(np.array(matrix), y_chebyshev)
+    coef = matrix.solve(sp.Matrix(y_chebyshev))
+
+    # first num_terms_numerator values are the numerator coefficients
+    # next num_terms_numerator - 1 values are the denominator coefficients
+    coef_numerator = coef[:num_terms_numerator]
+    coef_denominator = coef[num_terms_numerator:]
+
+    # h(x) = (p_0 + p_1 x + p_2 x^2) / (1 + q_1 x + q_2 x^2)
+    # Therefore, we insert 1 here.
+    coef_denominator = [1] + coef_denominator
+
+    return [coef_numerator, coef_denominator]
diff --git a/scripts/approximations/chebyshev.py b/scripts/approximations/chebyshev.py
new file mode 100644
index 000000000..0c2986d6f
--- /dev/null
+++ b/scripts/approximations/chebyshev.py
@@ -0,0 +1,29 @@
+import sympy as sp
+from typing import Tuple
+
+# N.B. We must evaluate x values early.
+# Otherwise, solving for the coefficients will take a long time.
+_x_eval_precision = 200
+_x_eval_chop = 1e-200
+
+def get_nodes(fn, x_start: sp.Float, x_end: sp.Float, num_terms: int) -> Tuple[list, list]:
+    """ Returns Chebyshev nodes between x_start and x_end with num_terms terms
+    and the given function fn.
+
+    Equation for Chebyshev nodes:
+    x_i = (x_start + x_end)/2 + (x_end - x_start)/2 * cos((2*i + 1)/(2*num_terms) * pi)
+    
+    The first returned value is a list of x coordinates for the Chebyshev nodes.
+    The second returned value is a list of y coordinates for the Chebyshev nodes.
+    """
+    x_estimated = []
+    y_estimated = []
+
+    for i in range (num_terms):
+        x_i = ((x_start + x_end) / 2 + (x_end - x_start) / 2 * sp.cos((2*sp.Float(i,_x_eval_precision) + 1).evalf(chop=_x_eval_chop) * sp.pi.evalf(chop=_x_eval_chop) / (2 * sp.Float(num_terms, _x_eval_precision))))
+        y_i = fn(x_i)
+
+        x_estimated.append(x_i)
+        y_estimated.append(y_i)
+
+    return x_estimated, y_estimated
diff --git a/scripts/approximations/main.py b/scripts/approximations/main.py
new file mode 100644
index 000000000..b91433855
--- /dev/null
+++ b/scripts/approximations/main.py
@@ -0,0 +1,194 @@
+import matplotlib.pyplot as plt
+import sympy as sp
+
+import approximations
+import rational
+
+##########################
+# Configuration Parameters
+
+# start of the interval to calculate the approximation on
+x_start = 0
+
+# end of the interval to calculate the approximation on
+x_end = 1
+
+# number of paramters to use for the approximations.
+num_parameters = 13
+
+# number of paramters to use for plotting error deltas.
+num_parameters_errors = 30
+
+# number of (x,y) coordinates used to plot the resulting approximation.
+num_points_plot = 100000
+
+# function to approximate
+approximated_fn = lambda x: sp.Pow(sp.E, x)
+
+# fixed point precision used in Osmosis `osmomath` package.
+osmomath_precision = 36
+
+# flag controlling whether to plot each approximation.
+# Plots if true.
+shouldPlotApproximations = True
+
+# flag controlling whether to compute max error for each approximation
+# given the equally spaced x coordinates.
+# Computes if true.
+shouldComputeErrorDelta = True
+
+# flag controlling whether to plot errors over a range.
+# Currently, does so only for Chebyshev Rational Approximation.
+# Computes if true.
+shouldPlotErrorRange  = True
+
+# flag controlling whether to plot max error for every approximation
+# with a varying number of parameters. This is useful to find the most
+# optimal number of parameters to use for each kind of approximation.
+# Plots if true.
+shouldPlotMaxError = True
+
+def plot_error_range(x_coordinates, y_approximation, y_actual):
+    """ Given x coordinates that correspond to approximated y coordinates and actual y coordinates,
+    compute the deltas between y approximated and y actual and plot them in log scale on y.
+    """
+    error_deltas = approximations.compute_error_range(y_approximation, y_actual)
+
+    plt.semilogy(x_coordinates, error_deltas)
+
+    plt.grid(True)
+    plt.title(f"Chebyshev Rational e^x Errors on [{x_start}, {x_end}]. {num_parameters} params, {num_points_plot} points")
+    plt.show()
+
+# This script does the following:
+# - Computes polynomial and rational approximations of a given function (e^x by default).
+# - Computes (x,y) coordinates for every approximation given the same x coordinates.
+# - Plots the results for rough comparison.
+# - Computes the max error for every approximation given the same x coordinates.
+# - Computes and plots max errors for every approximation with a varying number of parameters.
+# This script runs various approximation methods, plots their results and deltas
+# from actual function values. The script can also be configured to print the maximum error.
+# The exact behavior is controlled by the global
+# variables at the top of the file. 
+# The following are the resources used:
+# https://xn--2-umb.com/22/approximation/
+# https://sites.tufts.edu/atasissa/files/2019/09/remez.pdf
+def main():
+    # Equispaced x coordinates to be used for plotting every approximation.
+    x_coordinates = approximations.linspace(x_start, x_end, num_points_plot)
+
+    if shouldComputeErrorDelta or shouldPlotApproximations or shouldPlotErrorRange:
+        ###############################################
+        # Approximation With Given Number of Parameters
+        y_eqispaced_poly, y_chebyshev_poly, y_chebyshev_rational, y_actual = approximations.approx_and_eval_all(approximated_fn, num_parameters, x_coordinates)
+
+        ################
+        # Compute Errors
+        if shouldComputeErrorDelta:
+            print(F"\n\nMax Error on [{x_start}, {x_end}]")
+            print(F"{num_points_plot} coordinates equally spaced on the X axis")
+            print(F"{num_parameters} parameters used\n\n")
+
+            # Equispaced Polynomial Approximation
+            max_error_equispaced_poly = approximations.compute_max_error(y_eqispaced_poly, y_actual)
+            print(F"Equispaced Poly: {max_error_equispaced_poly.evalf(chop=1e-100)}")
+
+            # Chebyshev Polynomial Approximation
+            max_error_chebyshev_poly = approximations.compute_max_error(y_chebyshev_poly, y_actual)
+            print(F"Chebyshev Poly: {max_error_chebyshev_poly.evalf(chop=1e-100)}")
+
+            # Chebyshev Rational Approximation
+            max_error_chebyshev_rational = approximations.compute_max_error(y_chebyshev_rational, y_actual)
+            print(F"Chebyshev Rational: {max_error_chebyshev_rational.evalf(chop=1e-100)}")
+
+        if shouldPlotErrorRange:
+            plot_error_range(x_coordinates, y_chebyshev_rational, y_actual)
+
+        ###############################
+        # Plot Every Approximation Kind
+        if shouldPlotApproximations:
+            # Equispaced Polynomial Approximation
+            plt.plot(x_coordinates, y_eqispaced_poly, label="Equispaced Poly")
+
+            # Chebyshev Polynomial Approximation
+            plt.plot(x_coordinates, y_chebyshev_poly, label="Chebyshev Poly")
+
+            # Chebyshev Rational Approximation
+            plt.plot(x_coordinates, y_chebyshev_rational, label="Chebyshev Rational")
+
+            # Actual With Large Number of Coordinates (evenly spaced on the X-axis)
+            plt.plot(x_coordinates, y_actual, label=F"Actual")
+
+            plt.legend(loc="upper left")
+            plt.grid(True)
+            plt.title(f"Appproximation of e^x on [{x_start}, {x_end}] with {num_parameters} parameters")
+            plt.show()
+
+    #####################################################
+    # Calculate Errors Given Varying Number of Parameters
+    if shouldPlotMaxError:
+        x_axis = []
+
+        deltas_eqispaced_poly = []
+        deltas_chebyshev_poly = []
+        deltas_chebyshev_rational = []
+
+        ################
+        # Compute Deltas
+        # The deltas are taken from actual function values for different number of parameters
+        # This is needed to find the most optimal number of parameters to use.
+        for num_parameters_current in range(1, num_parameters_errors + 1):
+            x_axis.append(int(num_parameters_current))
+            y_eqispaced_poly, y_chebyshev_poly, y_chebyshev_rational, y_actual = approximations.approx_and_eval_all(approximated_fn, num_parameters_current, x_coordinates)
+
+            deltas_eqispaced_poly.append(approximations.compute_max_error(y_eqispaced_poly, y_actual))
+            deltas_chebyshev_poly.append(approximations.compute_max_error(y_chebyshev_poly, y_actual))
+            deltas_chebyshev_rational.append(approximations.compute_max_error(y_chebyshev_rational, y_actual))
+
+        ##################
+        # Plot the results
+
+        # Equispaced Polynomial Approximation
+        plt.semilogy(x_axis, deltas_eqispaced_poly, label="Equispaced Poly")
+
+        # Chebyshev Polynomial Approximation
+        plt.semilogy(x_axis, deltas_chebyshev_poly, label="Chebyshev Poly")
+
+        # Chebyshev Rational Approximation
+        plt.semilogy(x_axis, deltas_chebyshev_rational, label="Chebyshev Rational")
+
+        plt.legend(loc="upper left")
+        plt.grid(True)
+        plt.title(f"Approximation Errors on [{x_start}, {x_end}]")
+        plt.gca().invert_yaxis()
+        plt.xlabel('Number of Parameters')
+        plt.ylabel(F"-log_10{{ max | f'(x) - f(x) | }} where x is in [{x_start}, {x_end}]")
+        plt.show()
+
+# This script isolates the 13-parameter Chebyshev Rational approximation of e^x
+# We are planning to use it in production. Therefore, we need to peform coefficient
+# truncations to 36 decimal points (the max osmomath supported precision).
+def exponent_approximation_choice():
+    # Equispaced x coordinates to be used for plotting every approximation.
+    x_coordinates = approximations.linspace(x_start, x_end, num_points_plot)
+    x_coordinates = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in x_coordinates]
+
+    # Chebyshev Rational Approximation to get the coefficients.
+    coef_numerator, coef_denominator = approximations.chebyshev_rational_approx(approximated_fn, x_start, x_end, num_parameters)
+
+    # Truncate the coefficients to osmomath precision.
+    coef_numerator = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in coef_numerator]
+    coef_denominator = [sp.Float(sp.N(coef, osmomath_precision + 1), osmomath_precision + 1) for coef in coef_denominator]
+
+    # Evaluate approximation.
+    y_chebyshev_rational = rational.evaluate(x_coordinates, coef_numerator, coef_denominator)
+
+    # Compute Actual Values
+    y_actual = approximations.get_y_actual(approximated_fn, x_coordinates)
+
+    plot_error_range(x_coordinates, y_chebyshev_rational, y_actual)
+
+if __name__ == "__main__":
+    # Uncomment to run the main script.
+    #main()
+    exponent_approximation_choice()
diff --git a/scripts/approximations/polynomial.py b/scripts/approximations/polynomial.py
new file mode 100644
index 000000000..5c6af2043
--- /dev/null
+++ b/scripts/approximations/polynomial.py
@@ -0,0 +1,41 @@
+import sympy as sp
+
+def construct_vandermonde_matrix(x_list: list[sp.Float]) -> sp.Matrix:
+    """ Constructs a Vandermonde matrix for a polynomial approximation.
+    from the list of x values given.
+    
+    len(x_list) * len(x_list)
+    x_list is the list of all x values to construct the matrix from.
+    
+    V = [1 x_0 x_0^2 ... x_0^{n-1}]
+    [1 x_1 x_2^1 ... x_1^{n-1}]
+    ...
+    [1 x_n x_n^2 ... x_n^{n-1}]
+
+    Vandermonde matrix is a matrix with the terms of a geometric progression in each row.
+    We use Vandermonde matrix to convert the system of linear equations into a linear algebra problem
+    that we can solve.
+    """
+    num_terms = len(x_list)
+
+    matrix = []
+
+    for i in range(num_terms):
+        row = []
+        for j in range(num_terms):
+            row.append(sp.Pow(x_list[i], j))
+        matrix.append(row)
+
+    return sp.Matrix(matrix)
+
+def evaluate(x, coeffs):
+    """ Evaluates the polynomial. Given a list of x coordinates and a list of coefficients, returns a list of
+    y coordinates, one for each x coordinate. The coefficients must be in ascending order.
+    """
+    y = []
+    for x_i in x:
+        y_i = 0
+        for i in range(len(coeffs)):
+            y_i += coeffs[i]*sp.Pow(x_i, i)
+        y.append(y_i)
+    return y
diff --git a/scripts/approximations/rational.py b/scripts/approximations/rational.py
new file mode 100644
index 000000000..4adea2d91
--- /dev/null
+++ b/scripts/approximations/rational.py
@@ -0,0 +1,44 @@
+import sympy as sp
+
+import polynomial
+
+def construct_rational_eval_matrix(x_list: list, y_list: list, num_terms_numerator: int, num_terms_denominator) -> sp.Matrix:
+    """ Constructs a matrix to use for computing coefficients for a rational approximation
+        from the list of x and y values given.
+        len(x_list) * len(x_list)
+        x_list is the list of all x values to construct the matrix from.
+        V = [1 x_0 x_0^2 ... x_0^{n-1} - y_0*x_0 - y_0*x_0^2 ... - y_0*x_0^{n-1}]
+            [1 x_1 x_2^1 ... x_1^{n-1} - y_1*x_1 - y_1*x_1^2 ... - y_1*x_1^{n-1}]
+            ...
+            [1 x_n x_n^2 ... x_n^{n-1} - y_n*x_n - y_n*x_n^2 ... - y_n*x_n^{n-1}]
+    """
+    matrix = []
+
+    for i in range(num_terms_numerator + num_terms_denominator - 1):
+        row = []
+        for j in range(num_terms_numerator):
+            row.append(sp.Pow(x_list[i], j))
+
+        for j in range(num_terms_denominator):
+            # denominator terms
+            if j > 0:
+                row.append(-1 * sp.Pow(x_list[i], j) * y_list[i])
+
+        matrix.append(row)
+
+    return sp.Matrix(matrix)
+
+def evaluate(x: list, coefficients_numerator: list, coefficients_denominator: list):
+    """ Evaluates the rational function. Assume rational h(x) = p(x) / q(x)
+    Given a list of x coordinates, a list of coefficients of p(x) - coefficients_numerator, and a list of
+    coefficients of q(x) - coefficients_denominator, returns a list of y coordinates, one for each x coordinate.
+    """
+    p = polynomial.evaluate(x, coefficients_numerator)
+    q = polynomial.evaluate(x, coefficients_denominator)
+
+    result = []
+
+    for i in range(len(x)):
+        result.append(p[i] / q[i])
+
+    return result
diff --git a/scripts/approximations/requirements.txt b/scripts/approximations/requirements.txt
new file mode 100644
index 000000000..ce1140c3e
--- /dev/null
+++ b/scripts/approximations/requirements.txt
@@ -0,0 +1,16 @@
+contourpy==1.0.6
+cycler==0.11.0
+fonttools==4.38.0
+kiwisolver==1.4.4
+matplotlib==3.6.2
+mpmath==1.2.1
+numpy==1.23.5
+packaging==22.0
+Pillow==9.3.0
+pyparsing==3.0.9
+PyQt5==5.15.7
+PyQt5-Qt5==5.15.2
+PyQt5-sip==12.11.0
+python-dateutil==2.8.2
+six==1.16.0
+sympy==1.11.1
diff --git a/wasmbinding/stargate_whitelist.go b/wasmbinding/stargate_whitelist.go
index e3fd3fae7..6867e18f6 100644
--- a/wasmbinding/stargate_whitelist.go
+++ b/wasmbinding/stargate_whitelist.go
@@ -84,6 +84,7 @@ func init() {
 	setWhitelistedQuery("/osmosis.gamm.v1beta1.Query/PoolType", &gammtypes.QueryPoolTypeResponse{})
 	setWhitelistedQuery("/osmosis.gamm.v2.Query/SpotPrice", &gammv2types.QuerySpotPriceResponse{})
 	setWhitelistedQuery("/osmosis.gamm.v1beta1.Query/EstimateSwapExactAmountIn", &gammtypes.QuerySwapExactAmountInResponse{})
+	setWhitelistedQuery("/osmosis.gamm.v1beta1.Query/EstimateSwapExactAmountOut", &gammtypes.QuerySwapExactAmountOutResponse{})
 
 	// incentives
 	setWhitelistedQuery("/osmosis.incentives.Query/ModuleToDistributeCoins", &incentivestypes.ModuleToDistributeCoinsResponse{})
diff --git a/x/epochs/client/cli/query.go b/x/epochs/client/cli/query.go
index 9cce798cc..0eb082513 100644
--- a/x/epochs/client/cli/query.go
+++ b/x/epochs/client/cli/query.go
@@ -10,33 +10,24 @@ import (
 // GetQueryCmd returns the cli query commands for this module.
 func GetQueryCmd() *cobra.Command {
 	cmd := osmocli.QueryIndexCmd(types.ModuleName)
-
-	cmd.AddCommand(
-		GetCmdEpochsInfos(),
-		GetCmdCurrentEpoch(),
-	)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdEpochInfos)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdCurrentEpoch)
 
 	return cmd
 }
 
-func GetCmdEpochsInfos() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.QueryEpochsInfoRequest](
-		"epoch-infos",
-		"Query running epochInfos",
-		`{{.Short}}{{.ExampleHeader}}
-{{.CommandPrefix}} epoch-infos
-`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdEpochInfos() (*osmocli.QueryDescriptor, *types.QueryEpochsInfoRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "epoch-infos",
+		Short: "Query running epoch infos.",
+		Long: `{{.Short}}{{.ExampleHeader}}
+		{{.CommandPrefix}}`}, &types.QueryEpochsInfoRequest{}
 }
 
-func GetCmdCurrentEpoch() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.QueryCurrentEpochRequest](
-		"current-epoch [identifier]",
-		"Query current epoch by specified identifier",
-		`{{.Short}}{{.ExampleHeader}}
-{{.CommandPrefix}} current-epoch day
-`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdCurrentEpoch() (*osmocli.QueryDescriptor, *types.QueryCurrentEpochRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "current-epoch",
+		Short: "Query current epoch by specified identifier.",
+		Long: `{{.Short}}{{.ExampleHeader}}
+		{{.CommandPrefix}} day`}, &types.QueryCurrentEpochRequest{}
 }
diff --git a/x/gamm/client/cli/flags.go b/x/gamm/client/cli/flags.go
index 05ef34cac..20042d3e8 100644
--- a/x/gamm/client/cli/flags.go
+++ b/x/gamm/client/cli/flags.go
@@ -66,38 +66,30 @@ type smoothWeightChangeParamsInputs struct {
 	TargetPoolWeights string `json:"target-pool-weights"`
 }
 
-func FlagSetQuerySwapRoutes() *flag.FlagSet {
+func FlagSetMultihopSwapRoutes() *flag.FlagSet {
 	fs := flag.NewFlagSet("", flag.ContinueOnError)
-
 	fs.String(FlagSwapRoutePoolIds, "", "swap route pool id")
 	fs.String(FlagSwapRouteDenoms, "", "swap route amount")
 	return fs
 }
 
-func FlagSetSwapAmountOutRoutes() *flag.FlagSet {
+func FlagSetCreatePoolFile() *flag.FlagSet {
 	fs := flag.NewFlagSet("", flag.ContinueOnError)
-
-	fs.String(FlagSwapRoutePoolIds, "", "swap route pool ids")
-	fs.String(FlagSwapRouteDenoms, "", "swap route denoms")
+	fs.String(FlagPoolFile, "", "Pool json file path (if this path is given, other create pool flags should not be used)")
 	return fs
 }
 
-func FlagSetCreatePool() *flag.FlagSet {
+func FlagSetCreatePoolType() *flag.FlagSet {
 	fs := flag.NewFlagSet("", flag.ContinueOnError)
-
-	fs.String(FlagPoolFile, "", "Pool json file path (if this path is given, other create pool flags should not be used)")
 	fs.String(FlagPoolType, "uniswap", "Pool type (either \"balancer\", \"uniswap\", or \"stableswap\"")
-
 	return fs
 }
 
 func FlagSetJoinPool() *flag.FlagSet {
 	fs := flag.NewFlagSet("", flag.ContinueOnError)
-
 	fs.Uint64(FlagPoolId, 0, "The id of pool")
 	fs.String(FlagShareAmountOut, "", "Minimum amount of Gamm tokens to receive")
 	fs.StringArray(FlagMaxAmountsIn, []string{""}, "Maximum amount of each denom to send into the pool (specify multiple denoms with: --max-amounts-in=1uosmo --max-amounts-in=1uion)")
-
 	return fs
 }
 
diff --git a/x/gamm/client/cli/query.go b/x/gamm/client/cli/query.go
index b91c01ffe..4db205742 100644
--- a/x/gamm/client/cli/query.go
+++ b/x/gamm/client/cli/query.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/gogo/protobuf/proto"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
@@ -14,6 +15,7 @@ import (
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/cosmos/cosmos-sdk/version"
 	"github.com/spf13/cobra"
+	flag "github.com/spf13/pflag"
 	"gopkg.in/yaml.v2"
 
 	"github.com/osmosis-labs/osmosis/v13/osmoutils/osmocli"
@@ -24,17 +26,16 @@ import (
 // GetQueryCmd returns the cli query commands for this module.
 func GetQueryCmd() *cobra.Command {
 	cmd := osmocli.QueryIndexCmd(types.ModuleName)
-
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdSpotPrice)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdPool)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdPools)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdEstimateSwapExactAmountIn)
+	osmocli.AddQueryCmd(cmd, types.NewQueryClient, GetCmdEstimateSwapExactAmountOut)
 	cmd.AddCommand(
-		GetCmdPool(),
-		GetCmdPools(),
 		GetCmdNumPools(),
 		GetCmdPoolParams(),
 		GetCmdTotalShares(),
-		GetCmdSpotPrice(),
 		GetCmdQueryTotalLiquidity(),
-		GetCmdEstimateSwapExactAmountIn(),
-		GetCmdEstimateSwapExactAmountOut(),
 		GetCmdTotalPoolLiquidity(),
 		GetCmdQueryPoolsWithFilter(),
 		GetCmdPoolType(),
@@ -43,16 +44,12 @@ func GetQueryCmd() *cobra.Command {
 	return cmd
 }
 
-func GetCmdPool() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.QueryPoolRequest](
-		"pool [poolID]",
-		"Query pool",
-		`Query pool.
-Example:
-{{.CommandPrefix}} pool 1
-`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdPool() (*osmocli.QueryDescriptor, *types.QueryPoolRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "pool [poolID]",
+		Short: "Query pool",
+		Long: `{{.Short}}{{.ExampleHeader}}
+{{.CommandPrefix}} pool 1`}, &types.QueryPoolRequest{}
 }
 
 // TODO: Push this to the SDK.
@@ -77,14 +74,12 @@ func writeOutputBoilerplate(ctx client.Context, out []byte) error {
 	return nil
 }
 
-func GetCmdPools() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.QueryPoolsRequest](
-		"pools",
-		"Query pools",
-		`{{.Short}}{{.ExampleHeader}}
-{{.CommandPrefix}} pools`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdPools() (*osmocli.QueryDescriptor, *types.QueryPoolsRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "pools",
+		Short: "Query pools",
+		Long: `{{.Short}}{{.ExampleHeader}}
+{{.CommandPrefix}} pools`}, &types.QueryPoolsRequest{}
 }
 
 func GetCmdNumPools() *cobra.Command {
@@ -192,123 +187,76 @@ Example:
 	)
 }
 
-func GetCmdSpotPrice() *cobra.Command {
-	//nolint:staticcheck
-	return osmocli.SimpleQueryCmd[*types.QuerySpotPriceRequest](
-		"spot-price <pool-ID> [quote-asset-denom] [base-asset-denom]",
-		"Query spot-price (LEGACY, arguments are reversed!!)",
-		`Query spot price (Legacy).
-Example:
+//nolint:staticcheck
+func GetCmdSpotPrice() (*osmocli.QueryDescriptor, *types.QuerySpotPriceRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "spot-price <pool-ID> [quote-asset-denom] [base-asset-denom]",
+		Short: "Query spot-price (LEGACY, arguments are reversed!!)",
+		Long: `Query spot price (Legacy).{{.ExampleHeader}}
 {{.CommandPrefix}} spot-price 1 uosmo ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2
-`,
-		types.ModuleName, types.NewQueryClient,
-	)
+`}, &types.QuerySpotPriceRequest{}
 }
 
 // GetCmdEstimateSwapExactAmountIn returns estimation of output coin when amount of x token input.
-func GetCmdEstimateSwapExactAmountIn() *cobra.Command {
-	cmd := &cobra.Command{
+func GetCmdEstimateSwapExactAmountIn() (*osmocli.QueryDescriptor, *types.QuerySwapExactAmountInRequest) {
+	return &osmocli.QueryDescriptor{
 		Use:   "estimate-swap-exact-amount-in <poolID> <sender> <tokenIn>",
 		Short: "Query estimate-swap-exact-amount-in",
-		Long: strings.TrimSpace(
-			fmt.Sprintf(`Query estimate-swap-exact-amount-in.
-Example:
-$ %s query gamm estimate-swap-exact-amount-in 1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 stake --swap-route-pool-ids=2 --swap-route-pool-ids=3
-`,
-				version.AppName,
-			),
-		),
-		Args: cobra.ExactArgs(3),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			clientCtx, err := client.GetClientQueryContext(cmd)
-			if err != nil {
-				return err
-			}
-			queryClient := types.NewQueryClient(clientCtx)
-
-			poolID, err := strconv.Atoi(args[0])
-			if err != nil {
-				return err
-			}
-
-			routes, err := swapAmountInRoutes(cmd.Flags())
-			if err != nil {
-				return err
-			}
-
-			res, err := queryClient.EstimateSwapExactAmountIn(cmd.Context(), &types.QuerySwapExactAmountInRequest{
-				Sender:  args[1],        // TODO: where sender is used?
-				PoolId:  uint64(poolID), // TODO: is this poolId used?
-				TokenIn: args[2],
-				Routes:  routes,
-			})
-			if err != nil {
-				return err
-			}
-
-			return clientCtx.PrintProto(res)
-		},
-	}
-
-	cmd.Flags().AddFlagSet(FlagSetQuerySwapRoutes())
-	flags.AddQueryFlagsToCmd(cmd)
-	_ = cmd.MarkFlagRequired(FlagSwapRoutePoolIds)
-	_ = cmd.MarkFlagRequired(FlagSwapRouteDenoms)
-
-	return cmd
+		Long: `Query estimate-swap-exact-amount-in.{{.ExampleHeader}}
+{{.CommandPrefix}} estimate-swap-exact-amount-in 1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 stake --swap-route-pool-ids=2 --swap-route-pool-ids=3`,
+		ParseQuery: EstimateSwapExactAmountInParseArgs,
+		Flags:      osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetMultihopSwapRoutes()}},
+	}, &types.QuerySwapExactAmountInRequest{}
 }
 
 // GetCmdEstimateSwapExactAmountOut returns estimation of input coin to get exact amount of x token output.
-func GetCmdEstimateSwapExactAmountOut() *cobra.Command {
-	cmd := &cobra.Command{
+func GetCmdEstimateSwapExactAmountOut() (*osmocli.QueryDescriptor, *types.QuerySwapExactAmountOutRequest) {
+	return &osmocli.QueryDescriptor{
 		Use:   "estimate-swap-exact-amount-out <poolID> <sender> <tokenOut>",
 		Short: "Query estimate-swap-exact-amount-out",
-		Long: strings.TrimSpace(
-			fmt.Sprintf(`Query estimate-swap-exact-amount-out.
-Example:
-$ %s query gamm estimate-swap-exact-amount-out 1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 stake --swap-route-pool-ids=2 --swap-route-pool-ids=3
-`,
-				version.AppName,
-			),
-		),
-		Args: cobra.ExactArgs(3),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			clientCtx, err := client.GetClientQueryContext(cmd)
-			if err != nil {
-				return err
-			}
-			queryClient := types.NewQueryClient(clientCtx)
+		Long: `Query estimate-swap-exact-amount-out.{{.ExampleHeader}}
+{{.CommandPrefix}} estimate-swap-exact-amount-out 1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 stake --swap-route-pool-ids=2 --swap-route-pool-ids=3`,
+		ParseQuery: EstimateSwapExactAmountOutParseArgs,
+		Flags:      osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetMultihopSwapRoutes()}},
+	}, &types.QuerySwapExactAmountOutRequest{}
+}
 
-			poolID, err := strconv.Atoi(args[0])
-			if err != nil {
-				return err
-			}
+func EstimateSwapExactAmountInParseArgs(args []string, fs *flag.FlagSet) (proto.Message, error) {
+	poolID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return nil, err
+	}
 
-			routes, err := swapAmountOutRoutes(cmd.Flags())
-			if err != nil {
-				return err
-			}
+	routes, err := swapAmountInRoutes(fs)
+	if err != nil {
+		return nil, err
+	}
 
-			res, err := queryClient.EstimateSwapExactAmountOut(cmd.Context(), &types.QuerySwapExactAmountOutRequest{
-				Sender:   args[1],        // TODO: where sender is used?
-				PoolId:   uint64(poolID), // TODO: is this poolId used?
-				Routes:   routes,
-				TokenOut: args[2],
-			})
-			if err != nil {
-				return err
-			}
+	return &types.QuerySwapExactAmountInRequest{
+		Sender:  args[1],        // TODO: where sender is used?
+		PoolId:  uint64(poolID), // TODO: is this poolId used?
+		TokenIn: args[2],
+		Routes:  routes,
+	}, nil
+}
 
-			return clientCtx.PrintProto(res)
-		},
+func EstimateSwapExactAmountOutParseArgs(args []string, fs *flag.FlagSet) (proto.Message, error) {
+	poolID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return nil, err
 	}
 
-	cmd.Flags().AddFlagSet(FlagSetQuerySwapRoutes())
-	flags.AddQueryFlagsToCmd(cmd)
-	_ = cmd.MarkFlagRequired(FlagSwapRoutePoolIds)
-	_ = cmd.MarkFlagRequired(FlagSwapRouteDenoms)
+	routes, err := swapAmountOutRoutes(fs)
+	if err != nil {
+		return nil, err
+	}
 
-	return cmd
+	return &types.QuerySwapExactAmountOutRequest{
+		Sender:   args[1],        // TODO: where sender is used?
+		PoolId:   uint64(poolID), // TODO: is this poolId used?
+		Routes:   routes,
+		TokenOut: args[2],
+	}, nil
 }
 
 // GetCmdQueryPoolsWithFilter returns pool with filter
diff --git a/x/gamm/client/cli/tx.go b/x/gamm/client/cli/tx.go
index 23c3fbd31..cdff32213 100644
--- a/x/gamm/client/cli/tx.go
+++ b/x/gamm/client/cli/tx.go
@@ -16,33 +16,23 @@ import (
 	"github.com/osmosis-labs/osmosis/v13/x/gamm/types"
 
 	"github.com/cosmos/cosmos-sdk/client"
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/client/tx"
 	sdk "github.com/cosmos/cosmos-sdk/types"
 )
 
 func NewTxCmd() *cobra.Command {
-	txCmd := &cobra.Command{
-		Use:                        types.ModuleName,
-		Short:                      "Generalized automated market maker transaction subcommands",
-		DisableFlagParsing:         true,
-		SuggestionsMinimumDistance: 2,
-		RunE:                       client.ValidateCmd,
-	}
-
+	txCmd := osmocli.TxIndexCmd(types.ModuleName)
+	osmocli.AddTxCmd(txCmd, NewJoinPoolCmd)
+	osmocli.AddTxCmd(txCmd, NewExitPoolCmd)
+	osmocli.AddTxCmd(txCmd, NewSwapExactAmountInCmd)
+	osmocli.AddTxCmd(txCmd, NewSwapExactAmountOutCmd)
+	osmocli.AddTxCmd(txCmd, NewJoinSwapExternAmountIn)
+	osmocli.AddTxCmd(txCmd, NewJoinSwapShareAmountOut)
+	osmocli.AddTxCmd(txCmd, NewExitSwapExternAmountOut)
+	osmocli.AddTxCmd(txCmd, NewExitSwapShareAmountIn)
 	txCmd.AddCommand(
-		NewCreatePoolCmd(),
-		NewJoinPoolCmd(),
-		NewExitPoolCmd(),
-		NewSwapExactAmountInCmd(),
-		NewSwapExactAmountOutCmd(),
-		NewJoinSwapExternAmountIn(),
-		NewJoinSwapShareAmountOut(),
-		NewExitSwapExternAmountOut(),
-		NewExitSwapShareAmountIn(),
+		NewCreatePoolCmd().BuildCommandCustomFn(),
 		NewStableSwapAdjustScalingFactorsCmd(),
 	)
-
 	return txCmd
 }
 
@@ -50,12 +40,12 @@ var poolIdFlagOverride = map[string]string{
 	"poolid": FlagPoolId,
 }
 
-func NewCreatePoolCmd() *cobra.Command {
-	cmd := &cobra.Command{
+func NewCreatePoolCmd() *osmocli.TxCliDesc {
+	desc := osmocli.TxCliDesc{
 		Use:   "create-pool [flags]",
 		Short: "create a new pool and provide the liquidity to it",
-		Long:  `Must provide path to a pool JSON file (--pool-file) describing the pool to be created`,
-		Example: `Sample pool JSON file contents for balancer:
+		Long: `Must provide path to a pool JSON file (--pool-file) describing the pool to be created
+Sample pool JSON file contents for balancer:
 {
 	"weights": "4uatom,4osmo,2uakt",
 	"initial-deposit": "100uatom,5osmo,20uakt",
@@ -73,49 +63,18 @@ For stableswap (demonstrating need for a 1:1000 scaling factor, see doc)
 	"scaling-factors": "1000,1"
 }
 `,
-		Args: cobra.ExactArgs(0),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			clientCtx, err := client.GetClientTxContext(cmd)
-			if err != nil {
-				return err
-			}
-
-			txf := tx.NewFactoryCLI(clientCtx, cmd.Flags()).
-				WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever)
-
-			poolType, err := cmd.Flags().GetString(FlagPoolType)
-			if err != nil {
-				return err
-			}
-			poolType = strings.ToLower(poolType)
-
-			var msg sdk.Msg
-			if poolType == "balancer" || poolType == "uniswap" {
-				msg, err = NewBuildCreateBalancerPoolMsg(clientCtx, cmd.Flags())
-				if err != nil {
-					return err
-				}
-			} else if poolType == "stableswap" {
-				msg, err = NewBuildCreateStableswapPoolMsg(clientCtx, cmd.Flags())
-				if err != nil {
-					return err
-				}
-			}
-
-			return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg)
+		NumArgs:          0,
+		ParseAndBuildMsg: BuildCreatePoolCmd,
+		Flags: osmocli.FlagDesc{
+			RequiredFlags: []*flag.FlagSet{FlagSetCreatePoolFile()},
+			OptionalFlags: []*flag.FlagSet{FlagSetCreatePoolType()},
 		},
 	}
-
-	cmd.Flags().AddFlagSet(FlagSetCreatePool())
-	flags.AddTxFlagsToCmd(cmd)
-
-	_ = cmd.MarkFlagRequired(FlagPoolFile)
-
-	return cmd
+	return &desc
 }
 
-func NewJoinPoolCmd() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgJoinPool](&osmocli.TxCliDesc{
+func NewJoinPoolCmd() (*osmocli.TxCliDesc, *types.MsgJoinPool) {
+	return &osmocli.TxCliDesc{
 		Use:   "join-pool",
 		Short: "join a new pool and provide the liquidity to it",
 		CustomFlagOverrides: map[string]string{
@@ -124,17 +83,13 @@ func NewJoinPoolCmd() *cobra.Command {
 		},
 		CustomFieldParsers: map[string]osmocli.CustomFieldParserFn{
 			"TokenInMaxs": osmocli.FlagOnlyParser(maxAmountsInParser),
-		}})
-
-	cmd.Flags().AddFlagSet(FlagSetJoinPool())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	_ = cmd.MarkFlagRequired(FlagShareAmountOut)
-	_ = cmd.MarkFlagRequired(FlagMaxAmountsIn)
-	return cmd
+		},
+		Flags: osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetJoinPool()}},
+	}, &types.MsgJoinPool{}
 }
 
-func NewExitPoolCmd() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgExitPool](&osmocli.TxCliDesc{
+func NewExitPoolCmd() (*osmocli.TxCliDesc, *types.MsgExitPool) {
+	return &osmocli.TxCliDesc{
 		Use:   "exit-pool",
 		Short: "exit a new pool and withdraw the liquidity from it",
 		CustomFlagOverrides: map[string]string{
@@ -143,90 +98,67 @@ func NewExitPoolCmd() *cobra.Command {
 		},
 		CustomFieldParsers: map[string]osmocli.CustomFieldParserFn{
 			"TokenOutMins": osmocli.FlagOnlyParser(minAmountsOutParser),
-		}})
-
-	cmd.Flags().AddFlagSet(FlagSetExitPool())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	_ = cmd.MarkFlagRequired(FlagShareAmountIn)
-	_ = cmd.MarkFlagRequired(FlagMinAmountsOut)
-	return cmd
+		},
+		Flags: osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetExitPool()}},
+	}, &types.MsgExitPool{}
 }
 
-func NewSwapExactAmountInCmd() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgSwapExactAmountIn](&osmocli.TxCliDesc{
+func NewSwapExactAmountInCmd() (*osmocli.TxCliDesc, *types.MsgSwapExactAmountIn) {
+	return &osmocli.TxCliDesc{
 		Use:   "swap-exact-amount-in [token-in] [token-out-min-amount]",
 		Short: "swap exact amount in",
 		CustomFieldParsers: map[string]osmocli.CustomFieldParserFn{
 			"Routes": osmocli.FlagOnlyParser(swapAmountInRoutes),
-		}})
-
-	cmd.Flags().AddFlagSet(FlagSetQuerySwapRoutes())
-	_ = cmd.MarkFlagRequired(FlagSwapRoutePoolIds)
-	_ = cmd.MarkFlagRequired(FlagSwapRouteDenoms)
-	return cmd
+		},
+		Flags: osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetMultihopSwapRoutes()}},
+	}, &types.MsgSwapExactAmountIn{}
 }
 
-func NewSwapExactAmountOutCmd() *cobra.Command {
+func NewSwapExactAmountOutCmd() (*osmocli.TxCliDesc, *types.MsgSwapExactAmountOut) {
 	// Can't get rid of this parser without a break, because the args are out of order.
-	cmd := osmocli.TxCliDesc{
+	return &osmocli.TxCliDesc{
 		Use:              "swap-exact-amount-out [token-out] [token-in-max-amount]",
 		Short:            "swap exact amount out",
 		NumArgs:          2,
 		ParseAndBuildMsg: NewBuildSwapExactAmountOutMsg,
-	}.BuildCommandCustomFn()
-
-	cmd.Flags().AddFlagSet(FlagSetSwapAmountOutRoutes())
-	_ = cmd.MarkFlagRequired(FlagSwapRoutePoolIds)
-	_ = cmd.MarkFlagRequired(FlagSwapRouteDenoms)
-	return cmd
+		Flags:            osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetMultihopSwapRoutes()}},
+	}, &types.MsgSwapExactAmountOut{}
 }
 
-func NewJoinSwapExternAmountIn() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgJoinSwapExternAmountIn](&osmocli.TxCliDesc{
+func NewJoinSwapExternAmountIn() (*osmocli.TxCliDesc, *types.MsgJoinSwapExternAmountIn) {
+	return &osmocli.TxCliDesc{
 		Use:                 "join-swap-extern-amount-in [token-in] [share-out-min-amount]",
 		Short:               "join swap extern amount in",
 		CustomFlagOverrides: poolIdFlagOverride,
-	})
-
-	cmd.Flags().AddFlagSet(FlagSetJustPoolId())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	return cmd
+		Flags:               osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetJustPoolId()}},
+	}, &types.MsgJoinSwapExternAmountIn{}
 }
 
-func NewJoinSwapShareAmountOut() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgJoinSwapShareAmountOut](&osmocli.TxCliDesc{
+func NewJoinSwapShareAmountOut() (*osmocli.TxCliDesc, *types.MsgJoinSwapShareAmountOut) {
+	return &osmocli.TxCliDesc{
 		Use:                 "join-swap-share-amount-out [token-in-denom] [token-in-max-amount] [share-out-amount]",
 		Short:               "join swap share amount out",
 		CustomFlagOverrides: poolIdFlagOverride,
-	})
-
-	cmd.Flags().AddFlagSet(FlagSetJustPoolId())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	return cmd
+		Flags:               osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetJustPoolId()}},
+	}, &types.MsgJoinSwapShareAmountOut{}
 }
 
-func NewExitSwapExternAmountOut() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgExitSwapExternAmountOut](&osmocli.TxCliDesc{
+func NewExitSwapExternAmountOut() (*osmocli.TxCliDesc, *types.MsgExitSwapExternAmountOut) {
+	return &osmocli.TxCliDesc{
 		Use:                 "exit-swap-extern-amount-out [token-out] [share-in-max-amount]",
 		Short:               "exit swap extern amount out",
 		CustomFlagOverrides: poolIdFlagOverride,
-	})
-
-	cmd.Flags().AddFlagSet(FlagSetJustPoolId())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	return cmd
+		Flags:               osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetJustPoolId()}},
+	}, &types.MsgExitSwapExternAmountOut{}
 }
 
-func NewExitSwapShareAmountIn() *cobra.Command {
-	cmd := osmocli.BuildTxCli[*types.MsgExitSwapShareAmountIn](&osmocli.TxCliDesc{
+func NewExitSwapShareAmountIn() (*osmocli.TxCliDesc, *types.MsgExitSwapShareAmountIn) {
+	return &osmocli.TxCliDesc{
 		Use:                 "exit-swap-share-amount-in [token-out-denom] [share-in-amount] [token-out-min-amount]",
 		Short:               "exit swap share amount in",
 		CustomFlagOverrides: poolIdFlagOverride,
-	})
-
-	cmd.Flags().AddFlagSet(FlagSetJustPoolId())
-	_ = cmd.MarkFlagRequired(FlagPoolId)
-	return cmd
+		Flags:               osmocli.FlagDesc{RequiredFlags: []*flag.FlagSet{FlagSetJustPoolId()}},
+	}, &types.MsgExitSwapShareAmountIn{}
 }
 
 // TODO: Change these flags to args. Required flags don't make that much sense.
@@ -245,6 +177,28 @@ func NewStableSwapAdjustScalingFactorsCmd() *cobra.Command {
 	return cmd
 }
 
+func BuildCreatePoolCmd(clientCtx client.Context, args []string, fs *flag.FlagSet) (sdk.Msg, error) {
+	poolType, err := fs.GetString(FlagPoolType)
+	if err != nil {
+		return nil, err
+	}
+	poolType = strings.ToLower(poolType)
+
+	var msg sdk.Msg
+	if poolType == "balancer" || poolType == "uniswap" {
+		msg, err = NewBuildCreateBalancerPoolMsg(clientCtx, fs)
+		if err != nil {
+			return nil, err
+		}
+	} else if poolType == "stableswap" {
+		msg, err = NewBuildCreateStableswapPoolMsg(clientCtx, fs)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return msg, nil
+}
+
 func NewBuildCreateBalancerPoolMsg(clientCtx client.Context, fs *flag.FlagSet) (sdk.Msg, error) {
 	pool, err := parseCreateBalancerPoolFlags(fs)
 	if err != nil {
diff --git a/x/incentives/client/cli/query.go b/x/incentives/client/cli/query.go
index 5f67881aa..b88f2f4ab 100644
--- a/x/incentives/client/cli/query.go
+++ b/x/incentives/client/cli/query.go
@@ -21,90 +21,77 @@ import (
 func GetQueryCmd() *cobra.Command {
 	// group incentives queries under a subcommand
 	cmd := osmocli.QueryIndexCmd(types.ModuleName)
-
-	cmd.AddCommand(
-		GetCmdGauges(),
-		GetCmdToDistributeCoins(),
-		GetCmdGaugeByID(),
-		GetCmdActiveGauges(),
-		GetCmdActiveGaugesPerDenom(),
-		GetCmdUpcomingGauges(),
-		GetCmdUpcomingGaugesPerDenom(),
-		GetCmdRewardsEst(),
-	)
+	qcGetter := types.NewQueryClient
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdGauges)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdToDistributeCoins)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdGaugeByID)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdActiveGauges)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdActiveGaugesPerDenom)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdUpcomingGauges)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdUpcomingGaugesPerDenom)
+	cmd.AddCommand(GetCmdRewardsEst())
 
 	return cmd
 }
 
 // GetCmdGauges returns all available gauges.
-func GetCmdGauges() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.GaugesRequest](
-		"gauges",
-		"Query available gauges",
-		`{{.Short}}`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdGauges() (*osmocli.QueryDescriptor, *types.GaugesRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "gauges",
+		Short: "Query all available gauges",
+		Long:  "{{.Short}}",
+	}, &types.GaugesRequest{}
 }
 
 // GetCmdToDistributeCoins returns coins that are going to be distributed.
-func GetCmdToDistributeCoins() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.ModuleToDistributeCoinsRequest](
-		"to-distribute-coins",
-		"Query coins that is going to be distributed",
-		`{{.Short}}`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdToDistributeCoins() (*osmocli.QueryDescriptor, *types.ModuleToDistributeCoinsRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "to-distribute-coins",
+		Short: "Query coins that is going to be distributed",
+		Long:  `{{.Short}}`}, &types.ModuleToDistributeCoinsRequest{}
 }
 
 // GetCmdGaugeByID returns a gauge by ID.
-func GetCmdGaugeByID() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.GaugeByIDRequest](
-		"gauge-by-id [id]",
-		"Query gauge by id.",
-		`{{.Short}}{{.ExampleHeader}}
+func GetCmdGaugeByID() (*osmocli.QueryDescriptor, *types.GaugeByIDRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "gauge-by-id [id]",
+		Short: "Query gauge by id.",
+		Long: `{{.Short}}{{.ExampleHeader}}
 {{.CommandPrefix}} gauge-by-id 1
-`, types.ModuleName, types.NewQueryClient)
+`}, &types.GaugeByIDRequest{}
 }
 
 // GetCmdActiveGauges returns active gauges.
-func GetCmdActiveGauges() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.ActiveGaugesRequest](
-		"active-gauges",
-		"Query active gauges",
-		`{{.Short}}`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdActiveGauges() (*osmocli.QueryDescriptor, *types.ActiveGaugesRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "active-gauges",
+		Short: "Query active gauges",
+		Long:  `{{.Short}}`}, &types.ActiveGaugesRequest{}
 }
 
 // GetCmdActiveGaugesPerDenom returns active gauges for a specified denom.
-func GetCmdActiveGaugesPerDenom() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.ActiveGaugesPerDenomRequest](
-		"active-gauges-per-denom [denom]",
-		"Query active gauges per denom",
-		`{{.Short}}{{.ExampleHeader}}
-{{.CommandPrefix}} active-gauges-per-denom gamm/pool/1`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdActiveGaugesPerDenom() (*osmocli.QueryDescriptor, *types.ActiveGaugesPerDenomRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "active-gauges-per-den [den]denom [denom]",
+		Short: "Query active gauges per denom",
+		Long: `{{.Short}}{{.ExampleHeader}}
+{{.CommandPrefix}} active-gauges-per-denom gamm/pool/1`}, &types.ActiveGaugesPerDenomRequest{}
 }
 
 // GetCmdUpcomingGauges returns scheduled gauges.
-func GetCmdUpcomingGauges() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.UpcomingGaugesRequest](
-		"upcoming-gauges",
-		"Query upcoming gauges",
-		`{{.Short}}`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdUpcomingGauges() (*osmocli.QueryDescriptor, *types.UpcomingGaugesRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "upcoming-gauges",
+		Short: "Query upcoming gauges",
+		Long:  `{{.Short}}`}, &types.UpcomingGaugesRequest{}
 }
 
 // GetCmdUpcomingGaugesPerDenom returns scheduled gauges for specified denom..
-func GetCmdUpcomingGaugesPerDenom() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.UpcomingGaugesPerDenomRequest](
-		"upcoming-gauges-per-denom [denom]",
-		"Query scheduled gauges per denom",
-		`{{.Short}}`,
-		types.ModuleName, types.NewQueryClient,
-	)
+func GetCmdUpcomingGaugesPerDenom() (*osmocli.QueryDescriptor, *types.UpcomingGaugesPerDenomRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "upcoming-gauges-per-denom [denom]",
+		Short: "Query scheduled gauges per denom",
+		Long:  `{{.Short}}`}, &types.UpcomingGaugesPerDenomRequest{}
 }
 
 // GetCmdRewardsEst returns rewards estimation.
diff --git a/x/lockup/client/cli/query.go b/x/lockup/client/cli/query.go
index 907638404..62b8dd2e7 100644
--- a/x/lockup/client/cli/query.go
+++ b/x/lockup/client/cli/query.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 
 	"github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/client/flags"
@@ -24,21 +25,22 @@ import (
 func GetQueryCmd() *cobra.Command {
 	cmd := osmocli.QueryIndexCmd(types.ModuleName)
 
+	qcGetter := types.NewQueryClient
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdModuleBalance)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdModuleLockedAmount)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdAccountUnlockingCoins)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdAccountLockedPastTime)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdAccountLockedPastTimeNotUnlockingOnly)
+	osmocli.AddQueryCmd(cmd, qcGetter, GetCmdTotalLockedByDenom)
 	cmd.AddCommand(
-		GetCmdModuleBalance(),
-		GetCmdModuleLockedAmount(),
 		GetCmdAccountUnlockableCoins(),
-		GetCmdAccountUnlockingCoins(),
 		GetCmdAccountLockedCoins(),
-		GetCmdAccountLockedPastTime(),
-		GetCmdAccountLockedPastTimeNotUnlockingOnly(),
 		GetCmdAccountUnlockedBeforeTime(),
 		GetCmdAccountLockedPastTimeDenom(),
 		GetCmdLockedByID(),
 		GetCmdAccountLockedLongerDuration(),
 		GetCmdAccountLockedLongerDurationNotUnlockingOnly(),
 		GetCmdAccountLockedLongerDurationDenom(),
-		GetCmdTotalLockedByDenom(),
 		GetCmdOutputLocksJson(),
 		GetCmdSyntheticLockupsByLockupID(),
 		GetCmdAccountLockedDuration(),
@@ -52,20 +54,20 @@ func GetQueryCmd() *cobra.Command {
 // GetCmdModuleBalance returns full balance of the lockup module.
 // Lockup module is where coins of locks are held.
 // This includes locked balance and unlocked balance of the module.
-func GetCmdModuleBalance() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.ModuleBalanceRequest](
-		"module-balance",
-		"Query module balance",
-		`{{.Short}}`, types.ModuleName, types.NewQueryClient)
+func GetCmdModuleBalance() (*osmocli.QueryDescriptor, *types.ModuleBalanceRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "module-balance",
+		Short: "Query module balance",
+		Long:  `{{.Short}}`}, &types.ModuleBalanceRequest{}
 }
 
 // GetCmdModuleLockedAmount returns locked balance of the module,
 // which are all the tokens not unlocking + tokens that are not finished unlocking.
-func GetCmdModuleLockedAmount() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.ModuleLockedAmountRequest](
-		"module-locked-amount",
-		"Query locked amount",
-		`{{.Short}}`, types.ModuleName, types.NewQueryClient)
+func GetCmdModuleLockedAmount() (*osmocli.QueryDescriptor, *types.ModuleLockedAmountRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "module-locked-amount",
+		Short: "Query locked amount",
+		Long:  `{{.Short}}`}, &types.ModuleLockedAmountRequest{}
 }
 
 // GetCmdAccountUnlockableCoins returns unlockable coins which has finsihed unlocking.
@@ -107,13 +109,12 @@ $ %s query lockup account-unlockable-coins <address>
 }
 
 // GetCmdAccountUnlockingCoins returns unlocking coins of a specific account.
-func GetCmdAccountUnlockingCoins() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.AccountUnlockingCoinsRequest](
-		"account-unlocking-coins <address>",
-		"Query account's unlocking coins",
-		`{{.Short}}{{.ExampleHeader}}
-{{.CommandPrefix}} account-unlocking-coins <address>
-`, types.ModuleName, types.NewQueryClient)
+func GetCmdAccountUnlockingCoins() (*osmocli.QueryDescriptor, *types.AccountUnlockingCoinsRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "account-unlocking-coins <address>",
+		Short: "Query account's unlocking coins",
+		Long: `{{.Short}}{{.ExampleHeader}}
+{{.CommandPrefix}} account-unlocking-coins <address>`}, &types.AccountUnlockingCoinsRequest{}
 }
 
 // GetCmdAccountLockedCoins returns locked coins that that are still in a locked state from the specified account.
@@ -127,25 +128,25 @@ func GetCmdAccountLockedCoins() *cobra.Command {
 }
 
 // GetCmdAccountLockedPastTime returns locks of an account with unlock time beyond timestamp.
-func GetCmdAccountLockedPastTime() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.AccountLockedPastTimeRequest](
-		"account-locked-pastime <address> <timestamp>",
-		"Query locked records of an account with unlock time beyond timestamp",
-		`{{.Short}}{{.ExampleHeader}}
+func GetCmdAccountLockedPastTime() (*osmocli.QueryDescriptor, *types.AccountLockedPastTimeRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "account-locked-pastime <address> <timestamp>",
+		Short: "Query locked records of an account with unlock time beyond timestamp",
+		Long: `{{.Short}}{{.ExampleHeader}}
 {{.CommandPrefix}} account-locked-pastime <address> <timestamp>
-`, types.ModuleName, types.NewQueryClient)
+`}, &types.AccountLockedPastTimeRequest{}
 }
 
 // GetCmdAccountLockedPastTimeNotUnlockingOnly returns locks of an account with unlock time beyond provided timestamp
 // amongst the locks that are in the unlocking queue.
-func GetCmdAccountLockedPastTimeNotUnlockingOnly() *cobra.Command {
-	return osmocli.SimpleQueryCmd[*types.AccountLockedPastTimeNotUnlockingOnlyRequest](
-		"account-locked-pastime-not-unlocking <address> <timestamp>",
-		"Query locked records of an account with unlock time beyond timestamp within not unlocking queue.",
-		`{{.Short}}
+func GetCmdAccountLockedPastTimeNotUnlockingOnly() (*osmocli.QueryDescriptor, *types.AccountLockedPastTimeNotUnlockingOnlyRequest) {
+	return &osmocli.QueryDescriptor{
+		Use:   "account-locked-pastime-not-unlocking <address> <timestamp>",
+		Short: "Query locked records of an account with unlock time beyond timestamp within not unlocking queue.",
+		Long: `{{.Short}}
 Timestamp is UNIX time in seconds.{{.ExampleHeader}}
 {{.CommandPrefix}} account-locked-pastime-not-unlocking <address> <timestamp>
-`, types.ModuleName, types.NewQueryClient)
+`}, &types.AccountLockedPastTimeNotUnlockingOnlyRequest{}
 }
 
 // GetCmdAccountUnlockedBeforeTime returns locks with unlock time before the provided timestamp.
@@ -181,7 +182,7 @@ func GetCmdLockedByID() *cobra.Command {
 		QueryFnName: "LockedByID",
 	}
 	q.Long = osmocli.FormatLongDesc(q.Long, osmocli.NewLongMetadata(types.ModuleName).WithShort(q.Short))
-	return osmocli.SimpleQueryFromDescriptor[*types.LockedRequest](q, types.NewQueryClient)
+	return osmocli.BuildQueryCli[*types.LockedRequest](&q, types.NewQueryClient)
 }
 
 // GetCmdSyntheticLockupsByLockupID returns synthetic lockups by lockup id.
@@ -226,8 +227,8 @@ func GetCmdAccountLockedLongerDurationDenom() *cobra.Command {
 		`{{.Short}}`, types.ModuleName, types.NewQueryClient)
 }
 
-func GetCmdTotalLockedByDenom() *cobra.Command {
-	cmd := osmocli.SimpleQueryFromDescriptor[*types.LockedDenomRequest](osmocli.QueryDescriptor{
+func GetCmdTotalLockedByDenom() (*osmocli.QueryDescriptor, *types.LockedDenomRequest) {
+	return &osmocli.QueryDescriptor{
 		Use:   "total-locked-of-denom <denom>",
 		Short: "Query locked amount for a specific denom bigger then duration provided",
 		Long: osmocli.FormatLongDescDirect(`{{.Short}}{{.ExampleHeader}}
@@ -235,11 +236,8 @@ func GetCmdTotalLockedByDenom() *cobra.Command {
 		CustomFlagOverrides: map[string]string{
 			"duration": FlagMinDuration,
 		},
-		QueryFnName: "LockedDenom",
-	}, types.NewQueryClient)
-
-	cmd.Flags().AddFlagSet(FlagSetMinDuration())
-	return cmd
+		Flags: osmocli.FlagDesc{OptionalFlags: []*pflag.FlagSet{FlagSetMinDuration()}},
+	}, &types.LockedDenomRequest{}
 }
 
 // GetCmdOutputLocksJson outputs all locks into a file called lock_export.json.
diff --git a/x/lockup/client/cli/tx.go b/x/lockup/client/cli/tx.go
index cc4aa4335..bd1df6746 100644
--- a/x/lockup/client/cli/tx.go
+++ b/x/lockup/client/cli/tx.go
@@ -11,50 +11,48 @@ import (
 // GetTxCmd returns the transaction commands for this module.
 func GetTxCmd() *cobra.Command {
 	cmd := osmocli.TxIndexCmd(types.ModuleName)
-	cmd.AddCommand(
-		NewLockTokensCmd(),
-		NewBeginUnlockingAllCmd(),
-		NewBeginUnlockByIDCmd(),
-		NewForceUnlockByIdCmd(),
-	)
+	osmocli.AddTxCmd(cmd, NewLockTokensCmd)
+	osmocli.AddTxCmd(cmd, NewBeginUnlockingAllCmd)
+	osmocli.AddTxCmd(cmd, NewBeginUnlockByIDCmd)
+	osmocli.AddTxCmd(cmd, NewForceUnlockByIdCmd)
 
 	return cmd
 }
 
-func NewLockTokensCmd() *cobra.Command {
-	return osmocli.BuildTxCli[*types.MsgLockTokens](&osmocli.TxCliDesc{
+func NewLockTokensCmd() (*osmocli.TxCliDesc, *types.MsgLockTokens) {
+	return &osmocli.TxCliDesc{
 		Use:   "lock-tokens [tokens]",
 		Short: "lock tokens into lockup pool from user account",
 		CustomFlagOverrides: map[string]string{
 			"duration": FlagDuration,
 		},
 		Flags: osmocli.FlagDesc{RequiredFlags: []*pflag.FlagSet{FlagSetLockTokens()}},
-	})
+	}, &types.MsgLockTokens{}
 }
 
 // TODO: We should change the Use string to be unlock-all
-func NewBeginUnlockingAllCmd() *cobra.Command {
-	return osmocli.BuildTxCli[*types.MsgBeginUnlockingAll](&osmocli.TxCliDesc{
+func NewBeginUnlockingAllCmd() (*osmocli.TxCliDesc, *types.MsgBeginUnlockingAll) {
+	return &osmocli.TxCliDesc{
 		Use:   "begin-unlock-tokens",
 		Short: "begin unlock not unlocking tokens from lockup pool for sender",
-	})
+	}, &types.MsgBeginUnlockingAll{}
 }
 
 // NewBeginUnlockByIDCmd unlocks individual period lock by ID.
-func NewBeginUnlockByIDCmd() *cobra.Command {
-	return osmocli.BuildTxCli[*types.MsgBeginUnlocking](&osmocli.TxCliDesc{
+func NewBeginUnlockByIDCmd() (*osmocli.TxCliDesc, *types.MsgBeginUnlocking) {
+	return &osmocli.TxCliDesc{
 		Use:   "begin-unlock-by-id [id]",
 		Short: "begin unlock individual period lock by ID",
 		CustomFlagOverrides: map[string]string{
 			"coins": FlagAmount,
 		},
 		Flags: osmocli.FlagDesc{OptionalFlags: []*pflag.FlagSet{FlagSetUnlockTokens()}},
-	})
+	}, &types.MsgBeginUnlocking{}
 }
 
 // NewForceUnlockByIdCmd force unlocks individual period lock by ID if proper permissions exist.
-func NewForceUnlockByIdCmd() *cobra.Command {
-	return osmocli.BuildTxCli[*types.MsgBeginUnlocking](&osmocli.TxCliDesc{
+func NewForceUnlockByIdCmd() (*osmocli.TxCliDesc, *types.MsgForceUnlock) {
+	return &osmocli.TxCliDesc{
 		Use:   "force-unlock-by-id [id]",
 		Short: "force unlocks individual period lock by ID",
 		Long:  "force unlocks individual period lock by ID. if no amount provided, entire lock is unlocked",
@@ -62,5 +60,5 @@ func NewForceUnlockByIdCmd() *cobra.Command {
 			"coins": FlagAmount,
 		},
 		Flags: osmocli.FlagDesc{OptionalFlags: []*pflag.FlagSet{FlagSetUnlockTokens()}},
-	})
+	}, &types.MsgForceUnlock{}
 }
diff --git a/x/protorev/keeper/routes.go b/x/protorev/keeper/routes.go
new file mode 100644
index 000000000..ac9c1535d
--- /dev/null
+++ b/x/protorev/keeper/routes.go
@@ -0,0 +1,196 @@
+package keeper
+
+import (
+	"fmt"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
+	gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types"
+	"github.com/osmosis-labs/osmosis/v13/x/protorev/types"
+)
+
+type TradeInfo struct {
+	InputDenom  string
+	OutputDenom string
+	SwapFee     sdk.Dec
+	Pool        gammtypes.CFMMPoolI
+}
+
+type Route struct {
+	Trades []TradeInfo
+}
+
+// BuildRoutes builds all of the possible arbitrage routes given the tokenIn, tokenOut and poolId that were used in the swap
+func (k Keeper) BuildRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) []Route {
+	routes := make([]Route, 0)
+
+	// Append hot routes if they exist
+	if tokenPairRoutes, err := k.BuildTokenPairRoutes(ctx, tokenIn, tokenOut, poolId); err == nil {
+		routes = append(routes, tokenPairRoutes...)
+	}
+
+	// Append an osmo route if one exists
+	if osmoRoute, err := k.BuildOsmoRoute(ctx, tokenIn, tokenOut, poolId); err == nil {
+		routes = append(routes, osmoRoute)
+	}
+
+	// Append an atom route if one exists
+	if atomRoute, err := k.BuildAtomRoute(ctx, tokenIn, tokenOut, poolId); err == nil {
+		routes = append(routes, atomRoute)
+	}
+
+	return routes
+}
+
+// BuildTokenPairRoutes builds all of the possible arbitrage routes from the hot routes given the tokenIn, tokenOut and poolId that were used in the swap
+func (k Keeper) BuildTokenPairRoutes(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) ([]Route, error) {
+	// Get all of the routes from the store that match the given tokenIn and tokenOut
+	tokenPairArbRoutes, err := k.GetTokenPairArbRoutes(ctx, tokenIn, tokenOut)
+	if err != nil {
+		return []Route{}, err
+	}
+
+	// Iterate through all of the routes and build hot routes
+	routes := make([]Route, 0)
+	for _, route := range tokenPairArbRoutes.ArbRoutes {
+		newRoute, err := k.BuildTradeInfoHotRoute(ctx, route, tokenIn, tokenOut, poolId)
+		if err == nil {
+			routes = append(routes, newRoute)
+		}
+	}
+
+	return routes, nil
+}
+
+// BuildTradeInfoHotRoute constructs a cyclic arbitrage route given a hot route from the store and information about the swap that should be placed
+// in the hot route.
+func (k Keeper) BuildTradeInfoHotRoute(ctx sdk.Context, route *types.Route, tokenIn, tokenOut string, poolId uint64) (Route, error) {
+	newRoute := Route{Trades: make([]TradeInfo, len(route.Trades))}
+
+	for index, trade := range route.Trades {
+		var newTrade TradeInfo
+		// 0 is a placeholder for swaps that should be entered into the hot route
+		if trade.Pool == 0 {
+			pool, err := k.GetAndCheckPool(ctx, poolId)
+			if err != nil {
+				return Route{}, err
+			}
+
+			newTrade = TradeInfo{
+				InputDenom:  tokenOut,
+				OutputDenom: tokenIn,
+				SwapFee:     pool.GetSwapFee(ctx),
+				Pool:        pool,
+			}
+		} else {
+			pool, err := k.GetAndCheckPool(ctx, trade.Pool)
+			if err != nil {
+				return Route{}, err
+			}
+
+			newTrade = TradeInfo{
+				InputDenom:  trade.TokenIn,
+				OutputDenom: trade.TokenOut,
+				SwapFee:     pool.GetSwapFee(ctx),
+				Pool:        pool,
+			}
+		}
+
+		newRoute.Trades[index] = newTrade
+	}
+
+	if err := k.CheckValidHotRoute(newRoute); err != nil {
+		return Route{}, err
+	}
+	return newRoute, nil
+}
+
+// CheckValidHotRoute checks if the cyclic arbitrage route that was built using the hot routes method is correct. The criteria for a valid hot route is that
+// the in denom and out denom must be the same, in denom must be uosmo or atom, and there must be exactly three hops in the route
+func (k Keeper) CheckValidHotRoute(route Route) error {
+	if len(route.Trades) != 3 {
+		return fmt.Errorf("invalid hot route length")
+	}
+
+	if route.Trades[0].InputDenom != route.Trades[2].OutputDenom {
+		return fmt.Errorf("invalid hot route in and out denoms. in: %s, out: %s", route.Trades[0].InputDenom, route.Trades[2].OutputDenom)
+	}
+
+	if route.Trades[0].InputDenom != types.OsmosisDenomination && route.Trades[0].InputDenom != types.AtomDenomination {
+		return fmt.Errorf("invalid hot route in denom")
+	}
+
+	return nil
+}
+
+// BuildOsmoRoute builds a cyclic arbitrage route that starts and ends with osmo given the tokenIn, tokenOut and poolId that were used in the swap
+func (k Keeper) BuildOsmoRoute(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) (Route, error) {
+	return k.BuildTradeInfoRoute(ctx, types.OsmosisDenomination, tokenIn, tokenOut, poolId, k.GetOsmoPool)
+}
+
+// BuildAtomRoute builds a cyclic arbitrage route that starts and ends with atom given the tokenIn, tokenOut and poolId that were used in the swap
+func (k Keeper) BuildAtomRoute(ctx sdk.Context, tokenIn, tokenOut string, poolId uint64) (Route, error) {
+	return k.BuildTradeInfoRoute(ctx, types.AtomDenomination, tokenIn, tokenOut, poolId, k.GetAtomPool)
+}
+
+// BuildTradeInfoRoute constructs a cyclic arbitrage route that is starts/ends with swapDenom (atom or osmo) given the swap (tokenIn, tokenOut, poolId), and
+// a function that can get the poolId from the store given a (token, swapDenom) pair.
+func (k Keeper) BuildTradeInfoRoute(ctx sdk.Context, swapDenom, tokenIn, tokenOut string, poolId uint64, getPoolIDFromStore func(sdk.Context, string) (uint64, error)) (Route, error) {
+	// Creating the first trade in the arb
+	entryPoolId, err := getPoolIDFromStore(ctx, tokenOut)
+	if err != nil {
+		return Route{}, err
+	}
+	entryPool, err := k.GetAndCheckPool(ctx, entryPoolId)
+	if err != nil {
+		return Route{}, err
+	}
+	entryTrade := TradeInfo{
+		InputDenom:  swapDenom,
+		OutputDenom: tokenOut,
+		SwapFee:     entryPool.GetSwapFee(ctx),
+		Pool:        entryPool,
+	}
+
+	// Creating the second trade in the arb
+	middlePool, err := k.GetAndCheckPool(ctx, poolId)
+	if err != nil {
+		return Route{}, err
+	}
+	middleTrade := TradeInfo{
+		InputDenom:  tokenOut,
+		OutputDenom: tokenIn,
+		SwapFee:     middlePool.GetSwapFee(ctx),
+		Pool:        middlePool,
+	}
+
+	// Creating the third trade in the arb
+	exitPoolId, err := getPoolIDFromStore(ctx, tokenIn)
+	if err != nil {
+		return Route{}, err
+	}
+	exitPool, err := k.GetAndCheckPool(ctx, exitPoolId)
+	if err != nil {
+		return Route{}, err
+	}
+	exitTrade := TradeInfo{
+		InputDenom:  tokenIn,
+		OutputDenom: swapDenom,
+		SwapFee:     exitPool.GetSwapFee(ctx),
+		Pool:        exitPool,
+	}
+
+	return Route{Trades: []TradeInfo{entryTrade, middleTrade, exitTrade}}, nil
+}
+
+// GetAndCheckPool retrieves the pool from the x/gamm module given a poolId and ensures that the pool can be traded on
+func (k Keeper) GetAndCheckPool(ctx sdk.Context, poolId uint64) (gammtypes.CFMMPoolI, error) {
+	pool, err := k.gammKeeper.GetPoolAndPoke(ctx, poolId)
+	if err != nil {
+		return pool, err
+	}
+	if !pool.IsActive(ctx) {
+		return pool, fmt.Errorf("pool %d is not active", poolId)
+	}
+	return pool, nil
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b4f5b3cf6..15126de48 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -64,9 +64,12 @@ jobs:
         uses: technote-space/get-diff-action@v6.0.1
         with:
           PATTERNS: |
-            **/**.go
+            **/**.wasm
+            **/**!(test).go
+            tests/**/**.go
             go.mod
             go.sum
+            Makefile
       -
         name: Get data from build cache
         uses: actions/cache@v2
@@ -84,12 +87,15 @@ jobs:
           key: ${{ runner.os }}-go-docker-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-docker-${{ matrix.go-version }}-
+        if: env.GIT_DIFF
       -
         name: Set up QEMU
         uses: docker/setup-qemu-action@v2
+        if: env.GIT_DIFF
       -
         name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
+        if: env.GIT_DIFF
       -
         name: Build e2e image
         uses: docker/build-push-action@v3
@@ -102,6 +108,8 @@ jobs:
           cache-to: type=gha,mode=max
           build-args: |
             BASE_IMG_TAG=debug
+        if: env.GIT_DIFF
       -
         name: Test e2e and Upgrade
         run: make test-e2e-ci
+        if: env.GIT_DIFF
diff --git a/osmomath/decimal_test.go b/osmomath/decimal_test.go
index 1b09c9f0e..d881f86ba 100644
--- a/osmomath/decimal_test.go
+++ b/osmomath/decimal_test.go
@@ -24,6 +24,20 @@ func TestDecimalTestSuite(t *testing.T) {
 	suite.Run(t, new(decimalTestSuite))
 }
 
+// assertMutResult given expected value after applying a math operation, a start value,
+// mutative and non mutative results with start values, asserts that mutation are only applied
+// to the mutative versions. Also, asserts that both results match the expected value.
+func (s *decimalTestSuite) assertMutResult(expectedResult, startValue, mutativeResult, nonMutativeResult, mutativeStartValue, nonMutativeStartValue BigDec) {
+	// assert both results are as expected.
+	s.Require().Equal(expectedResult, mutativeResult)
+	s.Require().Equal(expectedResult, nonMutativeResult)
+
+	// assert that mutative method mutated the receiver
+	s.Require().Equal(mutativeStartValue, expectedResult)
+	// assert that non-mutative method did not mutate the receiver
+	s.Require().Equal(nonMutativeStartValue, startValue)
+}
+
 func TestDecApproxEq(t *testing.T) {
 	// d1 = 0.55, d2 = 0.6, tol = 0.1
 	d1 := NewDecWithPrec(55, 2)
@@ -1031,6 +1045,139 @@ func (s *decimalTestSuite) TestCustomBaseLog() {
 	}
 }
 
+func (s *decimalTestSuite) TestPowerInteger() {
+	var expectedErrTolerance = MustNewDecFromStr("0.000000000000000000000000000000100000")
+
+	tests := map[string]struct {
+		base           BigDec
+		exponent       uint64
+		expectedResult BigDec
+
+		expectedToleranceOverwrite BigDec
+	}{
+		"0^2": {
+			base:     ZeroDec(),
+			exponent: 2,
+
+			expectedResult: ZeroDec(),
+		},
+		"1^2": {
+			base:     OneDec(),
+			exponent: 2,
+
+			expectedResult: OneDec(),
+		},
+		"4^4": {
+			base:     MustNewDecFromStr("4"),
+			exponent: 4,
+
+			expectedResult: MustNewDecFromStr("256"),
+		},
+		"5^3": {
+			base:     MustNewDecFromStr("5"),
+			exponent: 4,
+
+			expectedResult: MustNewDecFromStr("625"),
+		},
+		"e^10": {
+			base:     eulersNumber,
+			exponent: 10,
+
+			// https://www.wolframalpha.com/input?i=e%5E10+41+digits
+			expectedResult: MustNewDecFromStr("22026.465794806716516957900645284244366354"),
+		},
+		"geom twap overflow: 2^log_2{max spot price + 1}": {
+			base: twoBigDec,
+			// add 1 for simplicity of calculation to isolate overflow.
+			exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice).Add(OneDec()).LogBase2().TruncateInt().Uint64()),
+
+			// https://www.wolframalpha.com/input?i=2%5E%28floor%28+log+base+2+%282%5E128%29%29%29+++39+digits
+			expectedResult: MustNewDecFromStr("340282366920938463463374607431768211456"),
+		},
+		"geom twap overflow: 2^log_2{max spot price}": {
+			base:     twoBigDec,
+			exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice).LogBase2().TruncateInt().Uint64()),
+
+			// https://www.wolframalpha.com/input?i=2%5E%28floor%28+log+base+2+%282%5E128+-+1%29%29%29+++39+digits
+			expectedResult: MustNewDecFromStr("170141183460469231731687303715884105728"),
+		},
+		"geom twap overflow: 2^log_2{max spot price / 2 - 2017}": { // 2017 is prime.
+			base:     twoBigDec,
+			exponent: uint64(BigDecFromSDKDec(gammtypes.MaxSpotPrice.Quo(sdk.NewDec(2)).Sub(sdk.NewDec(2017))).LogBase2().TruncateInt().Uint64()),
+
+			// https://www.wolframalpha.com/input?i=e%5E10+41+digits
+			expectedResult: MustNewDecFromStr("85070591730234615865843651857942052864"),
+		},
+
+		// sdk.Dec test vectors copied from osmosis-labs/cosmos-sdk:
+
+		"1.0 ^ (10) => 1.0": {
+			base:     OneDec(),
+			exponent: 10,
+
+			expectedResult: OneDec(),
+		},
+		"0.5 ^ 2 => 0.25": {
+			base:     NewDecWithPrec(5, 1),
+			exponent: 2,
+
+			expectedResult: NewDecWithPrec(25, 2),
+		},
+		"0.2 ^ 2 => 0.04": {
+			base:     NewDecWithPrec(2, 1),
+			exponent: 2,
+
+			expectedResult: NewDecWithPrec(4, 2),
+		},
+		"3 ^ 3 => 27": {
+			base:     NewBigDec(3),
+			exponent: 3,
+
+			expectedResult: NewBigDec(27),
+		},
+		"-3 ^ 4 = 81": {
+			base:     NewBigDec(-3),
+			exponent: 4,
+
+			expectedResult: NewBigDec(81),
+		},
+		"-3 ^ 50 = 717897987691852588770249": {
+			base:     NewBigDec(-3),
+			exponent: 50,
+
+			expectedResult: MustNewDecFromStr("717897987691852588770249"),
+		},
+		"-3 ^ 51 = -2153693963075557766310747": {
+			base:     NewBigDec(-3),
+			exponent: 51,
+
+			expectedResult: MustNewDecFromStr("-2153693963075557766310747"),
+		},
+		"1.414213562373095049 ^ 2 = 2": {
+			base:     NewDecWithPrec(1414213562373095049, 18),
+			exponent: 2,
+
+			expectedResult:             NewBigDec(2),
+			expectedToleranceOverwrite: MustNewDecFromStr("0.0000000000000000006"),
+		},
+	}
+
+	for name, tc := range tests {
+		tc := tc
+		s.Run(name, func() {
+
+			tolerance := expectedErrTolerance
+			if !tc.expectedToleranceOverwrite.IsNil() {
+				tolerance = tc.expectedToleranceOverwrite
+			}
+
+			actualResult := tc.base.PowerInteger(tc.exponent)
+			require.True(DecApproxEq(s.T(), tc.expectedResult, actualResult, tolerance))
+
+		})
+	}
+}
+
 func (s *decimalTestSuite) TestClone() {
 
 	// The value to change the underlying copy's
@@ -1099,16 +1246,51 @@ func (s *decimalTestSuite) TestMul_Mutation() {
 			startNonMut := tc.startValue.Clone()
 
 			resultMut := startMut.MulMut(mulBy)
-			result := startNonMut.Mul(mulBy)
+			resultNonMut := startNonMut.Mul(mulBy)
+
+			s.assertMutResult(tc.expectedMulResult, tc.startValue, resultMut, resultNonMut, startMut, startNonMut)
+		})
+	}
+}
+
+// TestMul_Mutation tests that PowerIntegerMut mutates the receiver
+// while PowerInteger is not.
+func (s *decimalTestSuite) TestPowerInteger_Mutation() {
+
+	exponent := uint64(2)
+
+	tests := map[string]struct {
+		startValue     BigDec
+		expectedResult BigDec
+	}{
+		"1": {
+			startValue:     OneDec(),
+			expectedResult: OneDec(),
+		},
+		"-3": {
+			startValue:     MustNewDecFromStr("-3"),
+			expectedResult: MustNewDecFromStr("9"),
+		},
+		"0": {
+			startValue:     ZeroDec(),
+			expectedResult: ZeroDec(),
+		},
+		"4": {
+			startValue:     MustNewDecFromStr("4.5"),
+			expectedResult: MustNewDecFromStr("20.25"),
+		},
+	}
+
+	for name, tc := range tests {
+		s.Run(name, func() {
+
+			startMut := tc.startValue.Clone()
+			startNonMut := tc.startValue.Clone()
 
-			// assert both results are as expectde.
-			s.Require().Equal(tc.expectedMulResult, resultMut)
-			s.Require().Equal(tc.expectedMulResult, result)
+			resultMut := startMut.PowerIntegerMut(exponent)
+			resultNonMut := startNonMut.PowerInteger(exponent)
 
-			// assert MulMut mutated the receiver
-			s.Require().Equal(tc.expectedMulResult, startMut)
-			// assert Mul did not mutate the receiver
-			s.Require().Equal(tc.startValue, startNonMut)
+			s.assertMutResult(tc.expectedResult, tc.startValue, resultMut, resultNonMut, startMut, startNonMut)
 		})
 	}
 }
diff --git a/osmoutils/osmocli/cli_tester.go b/osmoutils/osmocli/cli_tester.go
new file mode 100644
index 000000000..4ef6aaf9b
--- /dev/null
+++ b/osmoutils/osmocli/cli_tester.go
@@ -0,0 +1,111 @@
+package osmocli
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/cosmos/cosmos-sdk/client"
+	"github.com/cosmos/cosmos-sdk/client/flags"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/gogo/protobuf/proto"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"github.com/stretchr/testify/require"
+)
+
+type TxCliTestCase[M sdk.Msg] struct {
+	Cmd                    string
+	ExpectedMsg            M
+	ExpectedErr            bool
+	OnlyCheckValidateBasic bool
+}
+
+type QueryCliTestCase[Q proto.Message] struct {
+	Cmd           string
+	ExpectedQuery Q
+	ExpectedErr   bool
+}
+
+func RunTxTestCases[M sdk.Msg](t *testing.T, desc *TxCliDesc, testcases map[string]TxCliTestCase[M]) {
+	for name, tc := range testcases {
+		t.Run(name, func(t *testing.T) {
+			RunTxTestCase(t, desc, &tc)
+		})
+	}
+}
+
+func RunQueryTestCases[Q proto.Message](t *testing.T, desc *QueryDescriptor, testcases map[string]QueryCliTestCase[Q]) {
+	for name, tc := range testcases {
+		t.Run(name, func(t *testing.T) {
+			RunQueryTestCase(t, desc, &tc)
+		})
+	}
+}
+
+func RunTxTestCase[M sdk.Msg](t *testing.T, desc *TxCliDesc, tc *TxCliTestCase[M]) {
+	cmd := BuildTxCli[M](desc)
+	err := resetCommandFlagValues(cmd)
+	require.NoError(t, err, "error in resetCommandFlagValues")
+	args := strings.Split(tc.Cmd, " ")
+	err = cmd.Flags().Parse(args)
+	require.NoError(t, err, "error in cmd.Flags().Parse(args)")
+	clientCtx := newClientContextWithFrom(t, cmd.Flags())
+
+	msg, err := desc.ParseAndBuildMsg(clientCtx, args, cmd.Flags())
+	if tc.ExpectedErr {
+		require.Error(t, err)
+		return
+	}
+	require.NoError(t, err, "error in desc.ParseAndBuildMsg")
+	if tc.OnlyCheckValidateBasic {
+		require.NoError(t, msg.ValidateBasic())
+		return
+	}
+
+	require.Equal(t, tc.ExpectedMsg, msg)
+}
+
+func RunQueryTestCase[Q proto.Message](t *testing.T, desc *QueryDescriptor, tc *QueryCliTestCase[Q]) {
+	cmd := BuildQueryCli[Q, int](desc, nil)
+	err := resetCommandFlagValues(cmd)
+	require.NoError(t, err, "error in resetCommandFlagValues")
+	args := strings.Split(tc.Cmd, " ")
+	err = cmd.Flags().Parse(args)
+	require.NoError(t, err, "error in cmd.Flags().Parse(args)")
+
+	req, err := desc.ParseQuery(args, cmd.Flags())
+	if tc.ExpectedErr {
+		require.Error(t, err)
+		return
+	}
+	require.NoError(t, err, "error in desc.ParseQuery")
+	require.Equal(t, tc.ExpectedQuery, req)
+}
+
+// This logic is copied from the SDK, it should've just been publicly exposed.
+// But instead its buried within a mega-method.
+func newClientContextWithFrom(t *testing.T, fs *pflag.FlagSet) client.Context {
+	clientCtx := client.Context{}
+	from, _ := fs.GetString(flags.FlagFrom)
+	fromAddr, fromName, _, err := client.GetFromFields(nil, from, true)
+	require.NoError(t, err)
+
+	clientCtx = clientCtx.WithFrom(from).WithFromAddress(fromAddr).WithFromName(fromName)
+	return clientCtx
+}
+
+// taken from https://github.com/golang/debug/pull/8,
+// due to no cobra command for resetting flag value
+func resetCommandFlagValues(cmd *cobra.Command) error {
+	var retErr error = nil
+	cmd.Flags().VisitAll(func(f *pflag.Flag) {
+		if f.Changed {
+			err := f.Value.Set(f.DefValue)
+			if err != nil {
+				retErr = err
+			}
+			f.Changed = false
+		}
+	})
+	return retErr
+}
diff --git a/scripts/approximations/rational_test.py b/scripts/approximations/rational_test.py
new file mode 100644
index 000000000..099b15d5a
--- /dev/null
+++ b/scripts/approximations/rational_test.py
@@ -0,0 +1,36 @@
+import unittest
+import math
+import sympy as sp
+
+import rational
+
+class TestChebyshevRational(unittest.TestCase):
+
+    # assume (3,3) rational 
+    # h(x) = p(x) / q(x) = (p_0 + p_1 * x + p_2 * x^2) / (1 + q_1 * x + q_2 * x^2)
+    #  p_0 + p_1 * x + p_2 * x^2 - q_1 * y * x - q_2 * y * x^2 = y
+    # construct 5 x 5 matrix solving for p and q given x_0 to x_4 and y_0 to y_4
+    # assume function is y = e**x
+    def test_construct_matrix_3_3(self):
+        fn = lambda x: math.e**x
+        x = [1, 2, 3, 4, 5]
+        y = list(map(fn, x))
+
+        coeffs = rational.construct_rational_eval_matrix(x, y, 3, 3)
+        
+        # correct matrix size
+        self.assertEqual(len(x) * len(x), len(coeffs))
+
+        # first row is correct
+        for i in range(len(x)):
+            x_i = x[i]
+            y_i = y[i]
+
+            expected_row = [sp.Pow(x_i, 0), sp.Pow(x_i, 1), sp.Pow(x_i, 2), -1 * sp.Pow(x_i, 1) * y_i, -1 * sp.Pow(x_i, 2) * y_i]
+
+            actual_row = coeffs.row(i).tolist()[0]
+
+            self.assertEqual(expected_row, actual_row)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/x/epochs/client/cli/cli_test.go b/x/epochs/client/cli/cli_test.go
index a0e56a556..ffe28c308 100644
--- a/x/epochs/client/cli/cli_test.go
+++ b/x/epochs/client/cli/cli_test.go
@@ -1,113 +1,33 @@
 package cli_test
 
 import (
-	"github.com/gogo/protobuf/proto"
-	"github.com/stretchr/testify/suite"
+	"testing"
 
-	"github.com/osmosis-labs/osmosis/v13/app"
+	"github.com/osmosis-labs/osmosis/v13/osmoutils/osmocli"
 	"github.com/osmosis-labs/osmosis/v13/x/epochs/client/cli"
 	"github.com/osmosis-labs/osmosis/v13/x/epochs/types"
-
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	"github.com/cosmos/cosmos-sdk/testutil/network"
 )
 
-type IntegrationTestSuite struct {
-	suite.Suite
-
-	cfg     network.Config
-	network *network.Network
-}
-
-func (s *IntegrationTestSuite) SetupSuite() {
-	s.T().Log("setting up integration test suite")
-
-	s.cfg = app.DefaultConfig()
-
-	s.network = network.New(s.T(), s.cfg)
-
-	_, err := s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-}
-
-func (s *IntegrationTestSuite) TearDownSuite() {
-	s.T().Log("tearing down integration test suite")
-	s.network.Cleanup()
-}
-
-func (s *IntegrationTestSuite) TestGetCmdCurrentEpoch() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name       string
-		identifier string
-		expectErr  bool
-		respType   proto.Message
-	}{
-		{
-			"query weekly epoch number",
-			"weekly",
-			false, &types.QueryCurrentEpochResponse{},
+func TestGetCmdCurrentEpoch(t *testing.T) {
+	desc, _ := cli.GetCmdCurrentEpoch()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QueryCurrentEpochRequest]{
+		"basic test": {
+			Cmd: "day",
+			ExpectedQuery: &types.QueryCurrentEpochRequest{
+				Identifier: "day",
+			},
 		},
-		{
-			"query unavailable epoch number",
-			"unavailable",
-			false, &types.QueryCurrentEpochResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdCurrentEpoch()
-			clientCtx := val.ClientCtx
-
-			args := []string{
-				tc.identifier,
-			}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
 	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdEpochsInfos() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query epoch infos",
-			false, &types.QueryEpochsInfoResponse{},
+func TestGetCmdEpochsInfo(t *testing.T) {
+	desc, _ := cli.GetCmdEpochInfos()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QueryEpochsInfoRequest]{
+		"basic test": {
+			Cmd:           "",
+			ExpectedQuery: &types.QueryEpochsInfoRequest{},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdCurrentEpoch()
-			clientCtx := val.ClientCtx
-
-			args := []string{}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
diff --git a/x/gamm/client/cli/cli_test.go b/x/gamm/client/cli/cli_test.go
index 8958d2c57..d1795fca9 100644
--- a/x/gamm/client/cli/cli_test.go
+++ b/x/gamm/client/cli/cli_test.go
@@ -4,27 +4,22 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/gogo/protobuf/proto"
 	"github.com/stretchr/testify/suite"
 
-	"github.com/osmosis-labs/osmosis/v13/app"
 	"github.com/osmosis-labs/osmosis/v13/osmoutils"
+	"github.com/osmosis-labs/osmosis/v13/osmoutils/osmocli"
 	"github.com/osmosis-labs/osmosis/v13/x/gamm/client/cli"
-	gammtestutil "github.com/osmosis-labs/osmosis/v13/x/gamm/client/testutil"
+	"github.com/osmosis-labs/osmosis/v13/x/gamm/pool-models/balancer"
 	"github.com/osmosis-labs/osmosis/v13/x/gamm/types"
-	gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types"
 
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/crypto/hd"
-	"github.com/cosmos/cosmos-sdk/crypto/keyring"
 	"github.com/cosmos/cosmos-sdk/testutil"
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
 	"github.com/cosmos/cosmos-sdk/testutil/network"
 	sdk "github.com/cosmos/cosmos-sdk/types"
-	banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
-	tmcli "github.com/tendermint/tendermint/libs/cli"
+	"github.com/cosmos/cosmos-sdk/types/query"
 )
 
+var testAddresses = osmoutils.CreateRandomAccounts(3)
+
 type IntegrationTestSuite struct {
 	suite.Suite
 
@@ -32,115 +27,21 @@ type IntegrationTestSuite struct {
 	network *network.Network
 }
 
-func (s *IntegrationTestSuite) SetupSuite() {
-	s.T().Log("setting up integration test suite")
-
-	s.cfg = app.DefaultConfig()
-
-	// modification to pay fee with test bond denom "stake"
-	genesisState := app.ModuleBasics.DefaultGenesis(s.cfg.Codec)
-	gammGen := gammtypes.DefaultGenesis()
-	gammGen.Params.PoolCreationFee = sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 1000000)}
-	gammGenJson := s.cfg.Codec.MustMarshalJSON(gammGen)
-	genesisState[gammtypes.ModuleName] = gammGenJson
-	s.cfg.GenesisState = genesisState
-
-	s.network = network.New(s.T(), s.cfg)
-
-	_, err := s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	val := s.network.Validators[0]
-
-	// create a new pool
-	_, err = gammtestutil.MsgCreatePool(s.T(), val.ClientCtx, val.Address, "5stake,5node0token", "100stake,100node0token", "0.01", "0.01", "")
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-}
-
-func (s *IntegrationTestSuite) TearDownSuite() {
-	s.T().Log("tearing down integration test suite")
-	s.network.Cleanup()
-}
-
-func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewCreatePoolAddr",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewInt64Coin(s.cfg.BondDenom, 200000000), sdk.NewInt64Coin("node0token", 20000)), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		json         string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
+func TestNewCreatePoolCmd(t *testing.T) {
+	testCases := map[string]struct {
+		json      string
+		expectErr bool
 	}{
-		{
-			"one token pair pool",
-			fmt.Sprintf(`
-			{
-			  "%s": "1node0token",
-			  "%s": "100node0token",
-			  "%s": "0.001",
-			  "%s": "0.001"
-			}
-			`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee),
-			true, &sdk.TxResponse{}, 4,
-		},
-		{
-			"two tokens pair pool",
-			fmt.Sprintf(`
-			{
-			  "%s": "1node0token,3stake",
-			  "%s": "100node0token,100stake",
-			  "%s": "0.001",
-			  "%s": "0.001"
-			}
-			`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee),
-			false, &sdk.TxResponse{}, 0,
-		},
-		{
-			"change order of json fields",
-			fmt.Sprintf(`
-			{
-			  "%s": "100node0token,100stake",
-			  "%s": "0.001",
-			  "%s": "1node0token,3stake",
-			  "%s": "0.001"
-			}
-			`, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileWeights, cli.PoolFileExitFee),
-			false, &sdk.TxResponse{}, 0,
-		},
-		{ // --record-tokens=100.0stake2 --record-tokens=100.0stake --record-tokens-weight=5 --record-tokens-weight=5 --swap-fee=0.01 --exit-fee=0.01 --from=validator --keyring-backend=test --chain-id=testing --yes
-			"three tokens pair pool - insufficient balance check",
-			fmt.Sprintf(`
-			{
-			  "%s": "1node0token,1stake,2btc",
-			  "%s": "100node0token,100stake,100btc",
-			  "%s": "0.001",
-			  "%s": "0.001"
-			}
-			`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee),
-			false, &sdk.TxResponse{}, 5,
+		"two tokens pair pool": {
+			`{
+			  "weights": "1node0token,3stake",
+			  "initial-deposit": "100node0token,100stake",
+			  "swap-fee": "0.001",
+			  "exit-fee": "0.001"
+			}`,
+			false,
 		},
-		{
-			"future governor address",
+		"future governor address": {
 			fmt.Sprintf(`
 			{
 			  "%s": "1node0token,3stake",
@@ -150,56 +51,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 			  "%s": "osmo1fqlr98d45v5ysqgp6h56kpujcj4cvsjnjq9nck"
 			}
 			`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee, cli.PoolFileFutureGovernor),
-			false, &sdk.TxResponse{}, 0,
-		},
-		// Due to CI time concerns, we leave these CLI tests commented out, and instead guaranteed via
-		// the logic tests.
-		// {
-		// 	"future governor time",
-		// 	fmt.Sprintf(`
-		// 	{
-		// 	  "%s": "1node0token,3stake",
-		// 	  "%s": "100node0token,100stake",
-		// 	  "%s": "0.001",
-		// 	  "%s": "0.001",
-		// 	  "%s": "2h"
-		// 	}
-		// 	`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee, cli.PoolFileFutureGovernor),
-		// 	false, &sdk.TxResponse{}, 0,
-		// },
-		// {
-		// 	"future governor token + time",
-		// 	fmt.Sprintf(`
-		// 	{
-		// 	  "%s": "1node0token,3stake",
-		// 	  "%s": "100node0token,100stake",
-		// 	  "%s": "0.001",
-		// 	  "%s": "0.001",
-		// 	  "%s": "token,1000h"
-		// 	}
-		// 	`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee, cli.PoolFileFutureGovernor),
-		// 	false, &sdk.TxResponse{}, 0,
-		// },
-		// {
-		// 	"invalid future governor",
-		// 	fmt.Sprintf(`
-		// 	{
-		// 	  "%s": "1node0token,3stake",
-		// 	  "%s": "100node0token,100stake",
-		// 	  "%s": "0.001",
-		// 	  "%s": "0.001",
-		// 	  "%s": "validdenom,invalidtime"
-		// 	}
-		// 	`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee, cli.PoolFileFutureGovernor),
-		// 	true, &sdk.TxResponse{}, 7,
-		// },
-		{
-			"not valid json",
-			"bad json",
-			true, &sdk.TxResponse{}, 0,
+			false,
 		},
-		{
-			"bad pool json - missing quotes around exit fee",
+		"bad pool json - missing quotes around exit fee": {
 			fmt.Sprintf(`
 			{
 			  "%s": "1node0token,3stake",
@@ -208,14 +62,12 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 			  "%s": 0.001
 			}
 	`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee),
-			true, &sdk.TxResponse{}, 0,
+			true,
 		},
-		{
-			"empty pool json",
-			"", true, &sdk.TxResponse{}, 0,
+		"empty pool json": {
+			"", true,
 		},
-		{
-			"smooth change params",
+		"smooth change params": {
 			fmt.Sprintf(`
 				{
 					"%s": "1node0token,3stake",
@@ -231,10 +83,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 				`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee,
 				cli.PoolFileSmoothWeightChangeParams, cli.PoolFileDuration, cli.PoolFileTargetPoolWeights, cli.PoolFileStartTime,
 			),
-			false, &sdk.TxResponse{}, 0,
+			false,
 		},
-		{
-			"smooth change params - no start time",
+		"smooth change params - no start time": {
 			fmt.Sprintf(`
 				{
 					"%s": "1node0token,3stake",
@@ -249,10 +100,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 				`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee,
 				cli.PoolFileSmoothWeightChangeParams, cli.PoolFileDuration, cli.PoolFileTargetPoolWeights,
 			),
-			false, &sdk.TxResponse{}, 0,
+			false,
 		},
-		{
-			"empty smooth change params",
+		"empty smooth change params": {
 			fmt.Sprintf(`
 				{
 					"%s": "1node0token,3stake",
@@ -264,10 +114,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 				`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee,
 				cli.PoolFileSmoothWeightChangeParams,
 			),
-			false, &sdk.TxResponse{}, 0,
+			false,
 		},
-		{
-			"smooth change params wrong type",
+		"smooth change params wrong type": {
 			fmt.Sprintf(`
 				{
 					"%s": "1node0token,3stake",
@@ -279,10 +128,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 				`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee,
 				cli.PoolFileSmoothWeightChangeParams,
 			),
-			true, &sdk.TxResponse{}, 0,
+			true,
 		},
-		{
-			"smooth change params missing duration",
+		"smooth change params missing duration": {
 			fmt.Sprintf(`
 				{
 					"%s": "1node0token,3stake",
@@ -296,10 +144,9 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 				`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee,
 				cli.PoolFileSmoothWeightChangeParams, cli.PoolFileTargetPoolWeights,
 			),
-			true, &sdk.TxResponse{}, 0,
+			true,
 		},
-		{
-			"unknown fields in json",
+		"unknown fields in json": {
 			fmt.Sprintf(`
 			{
 			  "%s": "1node0token",
@@ -309,831 +156,223 @@ func (s *IntegrationTestSuite) TestNewCreatePoolCmd() {
 			  "unknown": true,
 			}
 			`, cli.PoolFileWeights, cli.PoolFileInitialDeposit, cli.PoolFileSwapFee, cli.PoolFileExitFee),
-			true, &sdk.TxResponse{}, 0,
+			true,
 		},
 	}
 
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewCreatePoolCmd()
-			clientCtx := val.ClientCtx
+	for name, tc := range testCases {
+		t.Run(name, func(tt *testing.T) {
+			desc := cli.NewCreatePoolCmd()
+			jsonFile := testutil.WriteToNewTempFile(tt, tc.json)
+			Cmd := fmt.Sprintf("--pool-file=%s --from=%s", jsonFile.Name(), testAddresses[0].String())
 
-			jsonFile := testutil.WriteToNewTempFile(s.T(), tc.json)
-
-			args := []string{
-				fmt.Sprintf("--%s=%s", cli.FlagPoolFile, jsonFile.Name()),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				osmoutils.DefaultFeeString(s.cfg),
-				fmt.Sprintf("--%s=%s", flags.FlagGas, fmt.Sprint(300000)),
-			}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType)
-				s.Require().NoError(err, out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
+			txTc := osmocli.TxCliTestCase[*balancer.MsgCreateBalancerPool]{
+				Cmd:                    Cmd,
+				ExpectedErr:            tc.expectErr,
+				OnlyCheckValidateBasic: true,
 			}
+			osmocli.RunTxTestCase(tt, desc, &txTc)
 		})
 	}
 }
 
-func (s IntegrationTestSuite) TestNewJoinPoolCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewJoinPoolAddr", keyring.English, sdk.FullFundraiserPath, "", hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewInt64Coin(s.cfg.BondDenom, 20000), sdk.NewInt64Coin("node0token", 20000)), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"join pool with insufficient balance",
-			[]string{
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagMaxAmountsIn, "100stake,100node0token"),
-				fmt.Sprintf("--%s=%s", cli.FlagShareAmountOut, "1000000000000000000000"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
-			},
-			false, &sdk.TxResponse{}, 6,
-		},
-		{
-			"join pool with sufficient balance",
-			[]string{ // join-pool --pool-id=1 --max-amounts-in=100stake --share-amount-out=100 --from=validator --keyring-backend=test --chain-id=testing --yes
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagMaxAmountsIn, "100stake,100node0token"),
-				fmt.Sprintf("--%s=%s", cli.FlagShareAmountOut, "10000000000000000000"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewJoinPoolCmd(t *testing.T) {
+	desc, _ := cli.NewJoinPoolCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgJoinPool]{
+		"join pool": {
+			Cmd: "--pool-id=1  --pool-id=1 --max-amounts-in=100stake --share-amount-out=100 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgJoinPool{
+				Sender:         testAddresses[0].String(),
+				PoolId:         1,
+				ShareOutAmount: sdk.NewIntFromUint64(100),
+				TokenInMaxs:    sdk.NewCoins(sdk.NewInt64Coin("stake", 100)),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewJoinPoolCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestNewExitPoolCmd() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"ask too much when exit",
-			[]string{ // --min-amounts-out=100stake --pool-id=1 --share-amount-in=10 --from=validator --keyring-backend=test --chain-id=testing --yes
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagShareAmountIn, "20000000000000000000"),
-				fmt.Sprintf("--%s=%s", cli.FlagMinAmountsOut, "20stake"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
-			},
-			false, &sdk.TxResponse{}, 7,
-		},
-		{
-			"ask enough when exit",
-			[]string{ // --min-amounts-out=100stake --pool-id=1 --share-amount-in=10 --from=validator --keyring-backend=test --chain-id=testing --yes
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagShareAmountIn, "20000000000000000000"),
-				fmt.Sprintf("--%s=%s", cli.FlagMinAmountsOut, "10stake"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
-			},
-			false, &sdk.TxResponse{}, 0,
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewExitPoolCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestNewSwapExactAmountOutCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewSwapExactAmountOut",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewInt64Coin(s.cfg.BondDenom, 20000), sdk.NewInt64Coin("node0token", 20000)), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name string
-		args []string
-
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"swap exact amount out", // osmosisd tx gamm swap-exact-amount-out 10stake 20 --swap-route-pool-ids=1 --swap-route-denoms=node0token --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"10stake", "20",
-				fmt.Sprintf("--%s=%d", cli.FlagSwapRoutePoolIds, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagSwapRouteDenoms, "node0token"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewExitPoolCmd(t *testing.T) {
+	desc, _ := cli.NewExitPoolCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgExitPool]{
+		"exit pool": {
+			Cmd: "--min-amounts-out=100stake --pool-id=1 --share-amount-in=10 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgExitPool{
+				Sender:        testAddresses[0].String(),
+				PoolId:        1,
+				ShareInAmount: sdk.NewIntFromUint64(10),
+				TokenOutMins:  sdk.NewCoins(sdk.NewInt64Coin("stake", 100)),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewSwapExactAmountOutCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestNewJoinSwapExternAmountInCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewJoinSwapExternAmountIn",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(20000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"join swap extern amount in", // osmosisd tx gamm join-swap-extern-amount-in --pool-id=1 10stake 1 --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"10stake", "1",
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewSwapExactAmountOutCmd(t *testing.T) {
+	desc, _ := cli.NewSwapExactAmountOutCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgSwapExactAmountOut]{
+		"swap exact amount out": {
+			Cmd: "10stake 20 --swap-route-pool-ids=1 --swap-route-denoms=node0token --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgSwapExactAmountOut{
+				Sender:           testAddresses[0].String(),
+				Routes:           []types.SwapAmountOutRoute{{PoolId: 1, TokenInDenom: "node0token"}},
+				TokenInMaxAmount: sdk.NewIntFromUint64(20),
+				TokenOut:         sdk.NewInt64Coin("stake", 10),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewJoinSwapExternAmountIn()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestNewExitSwapExternAmountOutCmd() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"exit swap extern amount out", // osmosisd tx gamm exit-swap-extern-amount-out --pool-id=1 10stake 1 --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"10stake", "10000000000000000000",
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewSwapExactAmountInCmd(t *testing.T) {
+	desc, _ := cli.NewSwapExactAmountInCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgSwapExactAmountIn]{
+		"swap exact amount in": {
+			Cmd: "10stake 3 --swap-route-pool-ids=1 --swap-route-denoms=node0token --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgSwapExactAmountIn{
+				Sender:            testAddresses[0].String(),
+				Routes:            []types.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: "node0token"}},
+				TokenIn:           sdk.NewInt64Coin("stake", 10),
+				TokenOutMinAmount: sdk.NewIntFromUint64(3),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewExitSwapExternAmountOut()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestNewJoinSwapShareAmountOutCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewJoinSwapShareAmountOutAddr", keyring.English,
-		sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(20000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"join swap share amount out", // osmosisd tx gamm join-swap-share-amount-out --pool-id=1 stake 10 1 --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"stake", "50", "5000000000000000000",
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewJoinSwapExternAmountInCmd(t *testing.T) {
+	desc, _ := cli.NewJoinSwapExternAmountIn()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgJoinSwapExternAmountIn]{
+		"swap exact amount in": {
+			Cmd: "10stake 1 --pool-id=1 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgJoinSwapExternAmountIn{
+				Sender:            testAddresses[0].String(),
+				PoolId:            1,
+				TokenIn:           sdk.NewInt64Coin("stake", 10),
+				ShareOutMinAmount: sdk.NewIntFromUint64(1),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewJoinSwapShareAmountOut()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestNewExitSwapShareAmountInCmd() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"exit swap share amount in", // osmosisd tx gamm exit-swap-share-amount-in --pool-id=1 stake 10 1 --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"stake", "10000000000000000000", "1",
-				fmt.Sprintf("--%s=%d", cli.FlagPoolId, 1),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestNewJoinSwapShareAmountOutCmd(t *testing.T) {
+	desc, _ := cli.NewJoinSwapShareAmountOut()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgJoinSwapShareAmountOut]{
+		"swap exact amount in": {
+			Cmd: "stake 10 1 --pool-id=1 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgJoinSwapShareAmountOut{
+				Sender:           testAddresses[0].String(),
+				PoolId:           1,
+				TokenInDenom:     "stake",
+				ShareOutAmount:   sdk.NewIntFromUint64(10),
+				TokenInMaxAmount: sdk.NewIntFromUint64(1),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewExitSwapShareAmountIn()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdPools() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query pools",
-			[]string{
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
+func TestNewExitSwapExternAmountOutCmd(t *testing.T) {
+	desc, _ := cli.NewExitSwapExternAmountOut()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgExitSwapExternAmountOut]{
+		"swap exact amount in": {
+			Cmd: "10stake 1 --pool-id=1 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgExitSwapExternAmountOut{
+				Sender:           testAddresses[0].String(),
+				PoolId:           1,
+				TokenOut:         sdk.NewInt64Coin("stake", 10),
+				ShareInMaxAmount: sdk.NewIntFromUint64(1),
 			},
-			false,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdPools() // osmosisd query gamm pools
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				resp := types.QueryPoolsResponse{}
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-
-				s.Require().Greater(len(resp.Pools), 0, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdNumPools() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query num-pools",
-			[]string{
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
+func TestNewExitSwapShareAmountInCmd(t *testing.T) {
+	desc, _ := cli.NewExitSwapShareAmountIn()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgExitSwapShareAmountIn]{
+		"swap exact amount in": {
+			Cmd: "stake 10 1 --pool-id=1 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgExitSwapShareAmountIn{
+				Sender:            testAddresses[0].String(),
+				PoolId:            1,
+				TokenOutDenom:     "stake",
+				ShareInAmount:     sdk.NewIntFromUint64(10),
+				TokenOutMinAmount: sdk.NewIntFromUint64(1),
 			},
-			false,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdNumPools() // osmosisd query gamm num-pools
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				resp := types.QueryNumPoolsResponse{}
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-
-				s.Require().Greater(resp.NumPools, uint64(0), out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdPool() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query pool by id", // osmosisd query gamm pool 1
-			[]string{
-				"1",
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
+func TestGetCmdPools(t *testing.T) {
+	desc, _ := cli.GetCmdPools()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QueryPoolsRequest]{
+		"basic test": {
+			Cmd: "--offset=2",
+			ExpectedQuery: &types.QueryPoolsRequest{
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
 			},
-			false,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdPool()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-
-				resp := types.QueryPoolResponse{}
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdTotalShares() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query pool total share by id", // osmosisd query gamm total-share 1
-			[]string{
-				"1",
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
-			},
-			false,
+func TestGetCmdPool(t *testing.T) {
+	desc, _ := cli.GetCmdPool()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QueryPoolRequest]{
+		"basic test": {
+			Cmd:           "1",
+			ExpectedQuery: &types.QueryPoolRequest{PoolId: 1},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdTotalShares()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				resp := types.QueryTotalSharesResponse{}
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdTotalLiquidity() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query total liquidity", // osmosisd query gamm total-liquidity
-			[]string{
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
+func TestGetCmdSpotPrice(t *testing.T) {
+	desc, _ := cli.GetCmdSpotPrice()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QuerySpotPriceRequest]{
+		"basic test": {
+			Cmd: "1 uosmo ibc/111",
+			ExpectedQuery: &types.QuerySpotPriceRequest{
+				PoolId:          1,
+				BaseAssetDenom:  "uosmo",
+				QuoteAssetDenom: "ibc/111",
 			},
-			false,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdQueryTotalLiquidity()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				resp := types.QueryTotalLiquidityResponse{}
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdSpotPrice() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-	}{
-		{
-			"query pool spot price", // osmosisd query gamm spot-price 1 stake node0token
-			[]string{
-				"1", "stake", "node0token",
-				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
+func TestGetCmdEstimateSwapExactAmountIn(t *testing.T) {
+	desc, _ := cli.GetCmdEstimateSwapExactAmountIn()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QuerySwapExactAmountInRequest]{
+		"basic test": {
+			Cmd: "1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 10stake --swap-route-pool-ids=2 --swap-route-denoms=node0token",
+			ExpectedQuery: &types.QuerySwapExactAmountInRequest{
+				Sender:  "osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7",
+				PoolId:  1,
+				TokenIn: "10stake",
+				Routes:  []types.SwapAmountInRoute{{PoolId: 2, TokenOutDenom: "node0token"}},
 			},
-			false,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdSpotPrice()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				resp := types.QuerySpotPriceResponse{}
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-// func (s *IntegrationTestSuite) TestGetCmdEstimateSwapExactAmountIn() {
-// 	val := s.network.Validators[0]
-
-// 	testCases := []struct {
-// 		name      string
-// 		args      []string
-// 		expectErr bool
-// 	}{
-// 		{
-// 			"query pool estimate swap exact amount in", // osmosisd query gamm estimate-swap-exact-amount-in 1 cosmos1n8skk06h3kyh550ad9qketlfhc2l5dsdevd3hq 10.0stake --swap-route-pool-ids=1 --swap-route-denoms=node0token
-// 			[]string{
-// 				"1",
-// 				"cosmos1n8skk06h3kyh550ad9qketlfhc2l5dsdevd3hq",
-// 				"10.0stake",
-// 				fmt.Sprintf("--%s=%d", cli.FlagSwapRoutePoolIds, 1),
-// 				fmt.Sprintf("--%s=%s", cli.FlagSwapRouteDenoms, "node0token"),
-// 				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
-// 			},
-// 			false,
-// 		},
-// 	}
-
-// 	for _, tc := range testCases {
-// 		tc := tc
-
-// 		s.Run(tc.name, func() {
-// 			cmd := cli.GetCmdEstimateSwapExactAmountIn()
-// 			clientCtx := val.ClientCtx
-
-// 			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-// 			if tc.expectErr {
-// 				s.Require().Error(err)
-// 			} else {
-// 				resp := types.QuerySwapExactAmountInResponse{}
-// 				s.Require().NoError(err, out.String())
-// 				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-// 			}
-// 		})
-// 	}
-// }
-
-// func (s *IntegrationTestSuite) TestGetCmdEstimateSwapExactAmountOut() {
-// 	val := s.network.Validators[0]
-
-// 	testCases := []struct {
-// 		name      string
-// 		args      []string
-// 		expectErr bool
-// 	}{
-// 		{
-// 			"query pool estimate swap exact amount in", // osmosisd query gamm estimate-swap-exact-amount-in 1 cosmos1n8skk06h3kyh550ad9qketlfhc2l5dsdevd3hq 10.0stake --swap-route-pool-ids=1 --swap-route-denoms=node0token
-// 			[]string{
-// 				"1",
-// 				val.Address.String(),
-// 				"10.0stake",
-// 				fmt.Sprintf("--%s=%d", cli.FlagSwapRoutePoolIds, 1),
-// 				fmt.Sprintf("--%s=%s", cli.FlagSwapRouteDenoms, "node0token"),
-// 				fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"),
-// 			},
-// 			false,
-// 		},
-// 	}
-
-// 	for _, tc := range testCases {
-// 		tc := tc
-
-// 		s.Run(tc.name, func() {
-// 			cmd := cli.GetCmdEstimateSwapExactAmountOut()
-// 			clientCtx := val.ClientCtx
-
-// 			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-// 			if tc.expectErr {
-// 				s.Require().Error(err)
-// 			} else {
-// 				resp := types.QuerySwapExactAmountOutResponse{}
-// 				s.Require().NoError(err, out.String())
-// 				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
-// 			}
-// 		})
-// 	}
-// }
-
-func (s IntegrationTestSuite) TestNewSwapExactAmountInCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewSwapExactAmountIn",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewInt64Coin(s.cfg.BondDenom, 20000), sdk.NewInt64Coin("node0token", 20000)), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name string
-		args []string
-
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"swap exact amount in", // osmosisd tx gamm swap-exact-amount-in 10stake 3 --swap-route-pool-ids=1 --swap-route-denoms=node0token --from=validator --keyring-backend=test --chain-id=testing --yes
-			[]string{
-				"10stake", "3",
-				fmt.Sprintf("--%s=%d", cli.FlagSwapRoutePoolIds, 1),
-				fmt.Sprintf("--%s=%s", cli.FlagSwapRouteDenoms, "node0token"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
+func TestGetCmdEstimateSwapExactAmountOut(t *testing.T) {
+	desc, _ := cli.GetCmdEstimateSwapExactAmountOut()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.QuerySwapExactAmountOutRequest]{
+		"basic test": {
+			Cmd: "1 osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7 10stake --swap-route-pool-ids=2 --swap-route-denoms=node0token",
+			ExpectedQuery: &types.QuerySwapExactAmountOutRequest{
+				Sender:   "osm11vmx8jtggpd9u7qr0t8vxclycz85u925sazglr7",
+				PoolId:   1,
+				TokenOut: "10stake",
+				Routes:   []types.SwapAmountOutRoute{{PoolId: 2, TokenInDenom: "node0token"}},
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewSwapExactAmountInCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
-}
-
-func TestIntegrationTestSuite(t *testing.T) {
-	suite.Run(t, new(IntegrationTestSuite))
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
diff --git a/x/gamm/client/testutil/test_helpers.go b/x/gamm/client/testutil/test_helpers.go
deleted file mode 100644
index 9e68396c2..000000000
--- a/x/gamm/client/testutil/test_helpers.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package testutil
-
-import (
-	"fmt"
-	"testing"
-
-	gammcli "github.com/osmosis-labs/osmosis/v13/x/gamm/client/cli"
-
-	"github.com/cosmos/cosmos-sdk/client"
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/testutil"
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	sdk "github.com/cosmos/cosmos-sdk/types"
-)
-
-// commonArgs is args for CLI test commands.
-var commonArgs = []string{
-	fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-	fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-	fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
-}
-
-// MsgCreatePool broadcast a pool creation message.
-func MsgCreatePool(
-	t *testing.T,
-	clientCtx client.Context,
-	owner fmt.Stringer,
-	tokenWeights string,
-	initialDeposit string,
-	swapFee string,
-	exitFee string,
-	futureGovernor string,
-	extraArgs ...string,
-) (testutil.BufferWriter, error) {
-	args := []string{}
-
-	jsonFile := testutil.WriteToNewTempFile(t,
-		fmt.Sprintf(`
-		{
-		  "%s": "%s",
-		  "%s": "%s",
-		  "%s": "%s",
-		  "%s": "%s",
-		  "%s": "%s"
-		}
-		`, gammcli.PoolFileWeights,
-			tokenWeights,
-			gammcli.PoolFileInitialDeposit,
-			initialDeposit,
-			gammcli.PoolFileSwapFee,
-			swapFee,
-			gammcli.PoolFileExitFee,
-			exitFee,
-			gammcli.PoolFileExitFee,
-			exitFee,
-		),
-	)
-
-	args = append(args,
-		fmt.Sprintf("--%s=%s", gammcli.FlagPoolFile, jsonFile.Name()),
-		fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()),
-		fmt.Sprintf("--%s=%d", flags.FlagGas, 300000),
-	)
-
-	args = append(args, commonArgs...)
-	return clitestutil.ExecTestCLICmd(clientCtx, gammcli.NewCreatePoolCmd(), args)
-}
-
-// MsgJoinPool broadcast pool join message.
-func MsgJoinPool(clientCtx client.Context, owner fmt.Stringer, poolID uint64, shareAmtOut string, maxAmountsIn []string, extraArgs ...string) (testutil.BufferWriter, error) {
-	args := []string{
-		fmt.Sprintf("--%s=%d", gammcli.FlagPoolId, poolID),
-		fmt.Sprintf("--%s=%s", gammcli.FlagShareAmountOut, shareAmtOut),
-		fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()),
-	}
-
-	for _, maxAmt := range maxAmountsIn {
-		args = append(args, fmt.Sprintf("--%s=%s", gammcli.FlagMaxAmountsIn, maxAmt))
-	}
-	args = append(args, commonArgs...)
-	return clitestutil.ExecTestCLICmd(clientCtx, gammcli.NewJoinPoolCmd(), args)
-}
-
-// MsgExitPool broadcast a pool exit message.
-func MsgExitPool(clientCtx client.Context, owner fmt.Stringer, poolID uint64, shareAmtIn string, minAmountsOut []string, extraArgs ...string) (testutil.BufferWriter, error) {
-	args := []string{
-		fmt.Sprintf("--%s=%d", gammcli.FlagPoolId, poolID),
-		fmt.Sprintf("--%s=%s", gammcli.FlagShareAmountIn, shareAmtIn),
-		fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()),
-	}
-
-	for _, maxAmt := range minAmountsOut {
-		args = append(args, fmt.Sprintf("--%s=%s", gammcli.FlagMinAmountsOut, maxAmt))
-	}
-
-	args = append(args, commonArgs...)
-	return clitestutil.ExecTestCLICmd(clientCtx, gammcli.NewExitPoolCmd(), args)
-}
diff --git a/x/incentives/client/cli/cli_test.go b/x/incentives/client/cli/cli_test.go
index a2d7e75e8..f79c26c5b 100644
--- a/x/incentives/client/cli/cli_test.go
+++ b/x/incentives/client/cli/cli_test.go
@@ -1,393 +1,96 @@
-package cli_test
+package cli
 
 import (
-	"fmt"
 	"testing"
 
-	"github.com/gogo/protobuf/proto"
-	"github.com/stretchr/testify/suite"
+	"github.com/cosmos/cosmos-sdk/types/query"
 
-	tmcli "github.com/tendermint/tendermint/libs/cli"
-
-	gammtypes "github.com/osmosis-labs/osmosis/v13/x/gamm/types"
-
-	sdk "github.com/cosmos/cosmos-sdk/types"
-
-	"github.com/osmosis-labs/osmosis/v13/app"
-	gammtestutil "github.com/osmosis-labs/osmosis/v13/x/gamm/client/testutil"
-	"github.com/osmosis-labs/osmosis/v13/x/incentives/client/cli"
+	"github.com/osmosis-labs/osmosis/v13/osmoutils"
+	"github.com/osmosis-labs/osmosis/v13/osmoutils/osmocli"
 	"github.com/osmosis-labs/osmosis/v13/x/incentives/types"
-	lockuptestutil "github.com/osmosis-labs/osmosis/v13/x/lockup/client/testutil"
-
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	"github.com/cosmos/cosmos-sdk/testutil/network"
 )
 
-type IntegrationTestSuite struct {
-	suite.Suite
-
-	cfg     network.Config
-	network *network.Network
-}
-
-func (s *IntegrationTestSuite) SetupSuite() {
-	s.T().Log("setting up integration test suite")
-
-	s.cfg = app.DefaultConfig()
-
-	// modification to pay pool creation fee with test bond denom "stake"
-	// marshal result into genesis json
-	genesisState := app.ModuleBasics.DefaultGenesis(s.cfg.Codec)
-	gammGen := gammtypes.DefaultGenesis()
-	gammGen.Params.PoolCreationFee = sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 1000000)}
-	gammGenJson := s.cfg.Codec.MustMarshalJSON(gammGen)
-	genesisState[gammtypes.ModuleName] = gammGenJson
-	s.cfg.GenesisState = genesisState
-
-	// create a network with a validator
-	s.network = network.New(s.T(), s.cfg)
-	val := s.network.Validators[0]
-
-	// create a pool to receive gamm tokens
-	_, err := gammtestutil.MsgCreatePool(s.T(), val.ClientCtx, val.Address, "5stake,5node0token", "100stake,100node0token", "0.01", "0.01", "")
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	lockAmt, err := sdk.ParseCoinNormalized(fmt.Sprint("100000gamm/pool/1"))
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	// lock gamm pool tokens to create lock ID 1
-	lockuptestutil.MsgLockTokens(val.ClientCtx, val.Address, lockAmt, "24h")
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	secondLockAmt, err := sdk.ParseCoinNormalized(fmt.Sprint("100000gamm/pool/1"))
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	// lock gamm pool tokens to create lock ID 2
-	lockuptestutil.MsgLockTokens(val.ClientCtx, val.Address, secondLockAmt, "168h")
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-}
-
-func (s *IntegrationTestSuite) TearDownSuite() {
-	s.T().Log("tearing down integration test suite")
-	s.network.Cleanup()
-}
-
-func (s *IntegrationTestSuite) TestGetCmdGauges() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		args      []string
-		respType  proto.Message
-	}{
-		{
-			"query gauges",
-			false,
-			[]string{fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			&types.GaugesResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdGauges()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
-
-func (s *IntegrationTestSuite) TestGetCmdToDistributeCoins() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query to distribute coins",
-			[]string{fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.ModuleToDistributeCoinsResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdToDistributeCoins()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
+var testAddresses = osmoutils.CreateRandomAccounts(3)
 
-func (s *IntegrationTestSuite) TestGetCmdGaugeByID() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query gauge by id",
-			[]string{"1", fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.GaugeByIDResponse{},
+func TestGetCmdGauges(t *testing.T) {
+	desc, _ := GetCmdGauges()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.GaugesRequest]{
+		"basic test": {
+			Cmd: "--offset=2",
+			ExpectedQuery: &types.GaugesRequest{
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
+			},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdGaugeByID()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdActiveGauges() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query active gauges",
-			[]string{fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.ActiveGaugesResponse{},
+func TestGetCmdToDistributeCoins(t *testing.T) {
+	desc, _ := GetCmdToDistributeCoins()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.ModuleToDistributeCoinsRequest]{
+		"basic test": {
+			Cmd: "", ExpectedQuery: &types.ModuleToDistributeCoinsRequest{},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdActiveGauges()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdActiveGaugesPerDenom() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query active gauges per denom",
-			[]string{s.cfg.BondDenom, fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.ActiveGaugesPerDenomResponse{},
+func TestGetCmdGaugeByID(t *testing.T) {
+	desc, _ := GetCmdGaugeByID()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.GaugeByIDRequest]{
+		"basic test": {
+			Cmd: "1", ExpectedQuery: &types.GaugeByIDRequest{Id: 1},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdActiveGaugesPerDenom()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdUpcomingGauges() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query upcoming gauges",
-			[]string{fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.UpcomingGaugesResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdUpcomingGauges()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
+func TestGetCmdActiveGauges(t *testing.T) {
+	desc, _ := GetCmdActiveGauges()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.ActiveGaugesRequest]{
+		"basic test": {
+			Cmd: "--offset=2",
+			ExpectedQuery: &types.ActiveGaugesRequest{
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
+			}},
 	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdUpcomingGaugesPerDenom() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query upcoming gauges per denom",
-			[]string{s.cfg.BondDenom, fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
-			false,
-			&types.UpcomingGaugesPerDenomResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdUpcomingGaugesPerDenom()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
+func TestGetCmdActiveGaugesPerDenom(t *testing.T) {
+	desc, _ := GetCmdActiveGaugesPerDenom()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.ActiveGaugesPerDenomRequest]{
+		"basic test": {
+			Cmd: "uosmo --offset=2",
+			ExpectedQuery: &types.ActiveGaugesPerDenomRequest{
+				Denom:      "uosmo",
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
+			}},
+	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestGetCmdRewardsEst() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		args      []string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query rewards estimation by owner",
-			[]string{
-				fmt.Sprintf("--%s=%s", cli.FlagOwner, val.Address.String()),
-				fmt.Sprintf("--%s=100", cli.FlagEndEpoch),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-			false,
-			&types.RewardsEstResponse{},
-		},
-		{
-			"query rewards estimation by lock id",
-			[]string{
-				fmt.Sprintf("--%s=1,2", cli.FlagLockIds),
-				fmt.Sprintf("--%s=100", cli.FlagEndEpoch),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-			false,
-			&types.RewardsEstResponse{},
-		},
-		{
-			"query rewards estimation with empty end epoch",
-			[]string{
-				fmt.Sprintf("--%s=1,2", cli.FlagLockIds),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-			false,
-			&types.RewardsEstResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdRewardsEst()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
+func TestGetCmdUpcomingGauges(t *testing.T) {
+	desc, _ := GetCmdUpcomingGauges()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.UpcomingGaugesRequest]{
+		"basic test": {
+			Cmd: "--offset=2",
+			ExpectedQuery: &types.UpcomingGaugesRequest{
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
+			}},
 	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func TestIntegrationTestSuite(t *testing.T) {
-	suite.Run(t, new(IntegrationTestSuite))
+func TestGetCmdUpcomingGaugesPerDenom(t *testing.T) {
+	desc, _ := GetCmdUpcomingGaugesPerDenom()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.UpcomingGaugesPerDenomRequest]{
+		"basic test": {
+			Cmd: "uosmo --offset=2",
+			ExpectedQuery: &types.UpcomingGaugesPerDenomRequest{
+				Denom:      "uosmo",
+				Pagination: &query.PageRequest{Key: []uint8{}, Offset: 2, Limit: 100},
+			}},
+	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
diff --git a/x/lockup/client/cli/cli_test.go b/x/lockup/client/cli/cli_test.go
index ff4f41ba7..a8bc958ca 100644
--- a/x/lockup/client/cli/cli_test.go
+++ b/x/lockup/client/cli/cli_test.go
@@ -1,740 +1,131 @@
-package cli_test
+package cli
 
 import (
-	"fmt"
 	"testing"
 	"time"
 
-	"github.com/gogo/protobuf/proto"
-	"github.com/stretchr/testify/suite"
-	tmcli "github.com/tendermint/tendermint/libs/cli"
+	sdk "github.com/cosmos/cosmos-sdk/types"
 
-	"github.com/osmosis-labs/osmosis/v13/app"
 	"github.com/osmosis-labs/osmosis/v13/osmoutils"
-	"github.com/osmosis-labs/osmosis/v13/x/lockup/client/cli"
-	lockuptestutil "github.com/osmosis-labs/osmosis/v13/x/lockup/client/testutil"
+	"github.com/osmosis-labs/osmosis/v13/osmoutils/osmocli"
 	"github.com/osmosis-labs/osmosis/v13/x/lockup/types"
-
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/crypto/hd"
-	"github.com/cosmos/cosmos-sdk/crypto/keyring"
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	"github.com/cosmos/cosmos-sdk/testutil/network"
-	sdk "github.com/cosmos/cosmos-sdk/types"
-	banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
 )
 
-type IntegrationTestSuite struct {
-	suite.Suite
-
-	cfg      network.Config
-	network  *network.Network
-	feeCoins sdk.Coins
-}
-
-func (s *IntegrationTestSuite) SetupSuite() {
-	s.T().Log("setting up integration test suite")
-
-	s.cfg = app.DefaultConfig()
-	s.network = network.New(s.T(), s.cfg)
-
-	_, err := s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	dayLockAmt, err := sdk.ParseCoinNormalized(fmt.Sprintf("200%s", s.network.Config.BondDenom))
-	s.Require().NoError(err)
-	secLockAmt, err := sdk.ParseCoinNormalized(fmt.Sprintf("11%s", s.network.Config.BondDenom))
-	s.Require().NoError(err)
-	thirdLockAmt, err := sdk.ParseCoinNormalized(fmt.Sprintf("12%s", s.network.Config.BondDenom))
-	s.Require().NoError(err)
-
-	s.feeCoins = sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)))
-
-	val := s.network.Validators[0]
-
-	// lock tokens for a day
-	_, err = lockuptestutil.MsgLockTokens(val.ClientCtx, val.Address, dayLockAmt, "24h")
-	s.Require().NoError(err)
-
-	// lock tokens for a second
-	_, err = lockuptestutil.MsgLockTokens(val.ClientCtx, val.Address, secLockAmt, "1s")
-	s.Require().NoError(err)
-
-	// lock tokens for a second
-	_, err = lockuptestutil.MsgLockTokens(val.ClientCtx, val.Address, thirdLockAmt, "1s")
-	s.Require().NoError(err)
-
-	// begin unlock all tokens
-	_, err = lockuptestutil.MsgBeginUnlocking(val.ClientCtx, val.Address)
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-}
-
-func (s *IntegrationTestSuite) TearDownSuite() {
-	s.T().Log("tearing down integration test suite")
-	s.network.Cleanup()
-}
-
-func (s *IntegrationTestSuite) TestNewLockTokensCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewValidator",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
+var testAddresses = osmoutils.CreateRandomAccounts(3)
 
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(20000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		fmt.Sprintf("--%s=%s", flags.FlagFees, s.feeCoins.String()),
-	)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"lock 201stake tokens for 1 day",
-			[]string{
-				sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(201))).String(),
-				fmt.Sprintf("--%s=%s", cli.FlagDuration, "24h"),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, s.feeCoins.String()),
+func TestLockTokensCmd(t *testing.T) {
+	desc, _ := NewLockTokensCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgLockTokens]{
+		"lock 201stake tokens for 1 day": {
+			Cmd: "201uosmo --duration=24h --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgLockTokens{
+				Owner:    testAddresses[0].String(),
+				Duration: time.Hour * 24,
+				Coins:    sdk.NewCoins(sdk.NewInt64Coin("uosmo", 201)),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewLockTokensCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestBeginUnlockingCmd() {
-	val := s.network.Validators[0]
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("BeginUnlockingAcc",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		val.ClientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(20000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	// lock tokens for a second
-	_, err = lockuptestutil.MsgLockTokens(val.ClientCtx, newAddr, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(200))), "1s")
-	s.Require().NoError(err)
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"begin unlocking",
-			[]string{
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, s.feeCoins.String()),
+func TestBeginUnlockingAllCmd(t *testing.T) {
+	desc, _ := NewBeginUnlockingAllCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgBeginUnlockingAll]{
+		"basic test": {
+			Cmd: "--from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgBeginUnlockingAll{
+				Owner: testAddresses[0].String(),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewBeginUnlockingAllCmd()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestNewBeginUnlockPeriodLockCmd() {
-	val := s.network.Validators[0]
-	clientCtx := val.ClientCtx
-
-	info, _, err := val.ClientCtx.Keyring.NewMnemonic("BeginUnlockPeriodLockAcc",
-		keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
-	s.Require().NoError(err)
-
-	newAddr := sdk.AccAddress(info.GetPubKey().Address())
-
-	_, err = banktestutil.MsgSendExec(
-		clientCtx,
-		val.Address,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(20000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-		fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-		osmoutils.DefaultFeeString(s.cfg),
-	)
-	s.Require().NoError(err)
-
-	// lock tokens for a second
-	txResp := sdk.TxResponse{}
-	out, err := lockuptestutil.MsgLockTokens(clientCtx,
-		newAddr,
-		sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(200))),
-		"1s")
-	s.Require().NoError(err)
-	s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String())
-	// This is a hardcoded path in the events to get the lockID
-	// this is incredibly brittle...
-	// fmt.Println(txResp.Logs[0])
-	lockID := txResp.Logs[0].Events[2].Attributes[0].Value
-
-	_, err = s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-
-	testCases := []struct {
-		name         string
-		args         []string
-		expectErr    bool
-		respType     proto.Message
-		expectedCode uint32
-	}{
-		{
-			"begin unlocking by id",
-			[]string{
-				lockID,
-				fmt.Sprintf("--%s=%s", cli.FlagAmount, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(100))).String()),
-				fmt.Sprintf("--%s=%s", flags.FlagFrom, newAddr),
-				// common args
-				fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-				fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-				fmt.Sprintf("--%s=%s", flags.FlagFees, s.feeCoins.String()),
+func TestBeginUnlockingByIDCmd(t *testing.T) {
+	desc, _ := NewBeginUnlockByIDCmd()
+	tcs := map[string]osmocli.TxCliTestCase[*types.MsgBeginUnlocking]{
+		"basic test no coins": {
+			Cmd: "10 --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgBeginUnlocking{
+				Owner: testAddresses[0].String(),
+				ID:    10,
+				Coins: sdk.Coins(nil),
 			},
-			false, &sdk.TxResponse{}, 0,
 		},
-	}
-	fmt.Println(testCases[0].args)
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.NewBeginUnlockByIDCmd()
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-
-				txResp := tc.respType.(*sdk.TxResponse)
-				s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
-			}
-		})
-	}
-}
-
-func (s *IntegrationTestSuite) TestCmdAccountUnlockableCoins() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name  string
-		args  []string
-		coins sdk.Coins
-	}{
-		{
-			"query validator account unlockable coins",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
+		"basic test w/ coins": {
+			Cmd: "10 --amount=5uosmo --from=" + testAddresses[0].String(),
+			ExpectedMsg: &types.MsgBeginUnlocking{
+				Owner: testAddresses[0].String(),
+				ID:    10,
+				Coins: sdk.NewCoins(sdk.NewInt64Coin("uosmo", 5)),
 			},
-			sdk.Coins{},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountUnlockableCoins()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountUnlockableCoinsResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(tc.coins.String(), result.Coins.String())
-		})
-	}
+	osmocli.RunTxTestCases(t, desc, tcs)
 }
 
-func (s *IntegrationTestSuite) TestCmdAccountUnlockingCoins() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name  string
-		args  []string
-		coins sdk.Coins
-	}{
-		{
-			"query validator account unlocking coins",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-			sdk.Coins{sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(200))},
+func TestModuleBalanceCmd(t *testing.T) {
+	desc, _ := GetCmdModuleBalance()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.ModuleBalanceRequest]{
+		"basic test": {
+			Cmd:           "",
+			ExpectedQuery: &types.ModuleBalanceRequest{},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountUnlockingCoins()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountUnlockingCoinsResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(tc.coins.String(), result.Coins.String())
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestCmdModuleBalance() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name  string
-		args  []string
-		coins sdk.Coins
-	}{
-		{
-			"query module balance",
-			[]string{
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
+func TestAccountUnlockingCoinsCmd(t *testing.T) {
+	desc, _ := GetCmdAccountUnlockingCoins()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.AccountUnlockingCoinsRequest]{
+		"basic test": {
+			Cmd: testAddresses[0].String(),
+			ExpectedQuery: &types.AccountUnlockingCoinsRequest{
+				Owner: testAddresses[0].String(),
 			},
-			sdk.Coins{sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(400))},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdModuleBalance()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.ModuleBalanceResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(tc.coins.String(), result.Coins.String())
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestCmdModuleLockedAmount() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name  string
-		args  []string
-		coins sdk.Coins
-	}{
-		{
-			"query module locked balance",
-			[]string{
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
+func TestCmdAccountLockedPastTime(t *testing.T) {
+	desc, _ := GetCmdAccountLockedPastTime()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.AccountLockedPastTimeRequest]{
+		"basic test": {
+			Cmd: testAddresses[0].String() + " 1670431012",
+			ExpectedQuery: &types.AccountLockedPastTimeRequest{
+				Owner:     testAddresses[0].String(),
+				Timestamp: time.Unix(1670431012, 0),
 			},
-			sdk.Coins{sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(400))},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdModuleLockedAmount()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.ModuleLockedAmountResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(tc.coins.String(), result.Coins.String())
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestCmdAccountLockedCoins() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name  string
-		args  []string
-		coins sdk.Coins
-	}{
-		{
-			"query account locked coins",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
+func TestCmdAccountLockedPastTimeNotUnlockingOnly(t *testing.T) {
+	desc, _ := GetCmdAccountLockedPastTimeNotUnlockingOnly()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.AccountLockedPastTimeNotUnlockingOnlyRequest]{
+		"basic test": {
+			Cmd: testAddresses[0].String() + " 1670431012",
+			ExpectedQuery: &types.AccountLockedPastTimeNotUnlockingOnlyRequest{
+				Owner:     testAddresses[0].String(),
+				Timestamp: time.Unix(1670431012, 0),
 			},
-			sdk.Coins{sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(200))},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedCoins()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.ModuleLockedAmountResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(tc.coins.String(), result.Coins.String())
-		})
-	}
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
 
-func (s IntegrationTestSuite) TestCmdAccountLockedPastTime() {
-	val := s.network.Validators[0]
-
-	timestamp := time.Now().Unix()
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"query account locked coins past time",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("%d", timestamp),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
+func TestCmdTotalLockedByDenom(t *testing.T) {
+	desc, _ := GetCmdTotalLockedByDenom()
+	tcs := map[string]osmocli.QueryCliTestCase[*types.LockedDenomRequest]{
+		"basic test": {
+			Cmd: "uosmo --min-duration=1s",
+			ExpectedQuery: &types.LockedDenomRequest{
+				Denom:    "uosmo",
+				Duration: time.Second,
 			},
 		},
 	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedPastTime()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedPastTimeResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 1)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountLockedPastTimeNotUnlockingOnly() {
-	val := s.network.Validators[0]
-
-	timestamp := time.Now().Unix()
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"query account locked coins past time not unlocking only",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("%d", timestamp),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedPastTimeNotUnlockingOnly()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedPastTimeNotUnlockingOnlyResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 0)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountUnlockedBeforeTime() {
-	val := s.network.Validators[0]
-
-	timestamp := time.Now().Unix()
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"query account locked coins before time",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("%d", timestamp),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountUnlockedBeforeTime()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountUnlockedBeforeTimeResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 0)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountLockedPastTimeDenom() {
-	val := s.network.Validators[0]
-
-	timestamp := time.Now().Unix()
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"query account locked coins past time denom",
-			[]string{
-				val.Address.String(),
-				fmt.Sprintf("%d", timestamp),
-				s.cfg.BondDenom,
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedPastTimeDenom()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedPastTimeDenomResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 1)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdLockedByID() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"get lock by id",
-			[]string{
-				fmt.Sprintf("%d", 1),
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdLockedByID()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.LockedResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Equal(result.Lock.ID, uint64(1))
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountLockedLongerDuration() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"get account locked longer than duration",
-			[]string{
-				val.Address.String(),
-				"1s",
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedLongerDuration()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedLongerDurationResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 1)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountLockedLongerDurationNotUnlockingOnly() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"get account locked longer than duration not unlocking only",
-			[]string{
-				val.Address.String(),
-				"1s",
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedLongerDurationNotUnlockingOnly()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedLongerDurationNotUnlockingOnlyResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 0)
-		})
-	}
-}
-
-func (s IntegrationTestSuite) TestCmdAccountLockedLongerDurationDenom() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name string
-		args []string
-	}{
-		{
-			"get account locked longer than duration denom",
-			[]string{
-				val.Address.String(),
-				"1s",
-				s.cfg.BondDenom,
-				fmt.Sprintf("--%s=json", tmcli.OutputFlag),
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdAccountLockedLongerDurationDenom()
-			clientCtx := val.ClientCtx
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
-			s.Require().NoError(err)
-
-			var result types.AccountLockedLongerDurationDenomResponse
-			s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
-			s.Require().Len(result.Locks, 1)
-		})
-	}
-}
-
-func TestIntegrationTestSuite(t *testing.T) {
-	suite.Run(t, new(IntegrationTestSuite))
+	osmocli.RunQueryTestCases(t, desc, tcs)
 }
diff --git a/x/lockup/client/cli/query_test.go b/x/lockup/client/cli/query_test.go
deleted file mode 100644
index d04f510ea..000000000
--- a/x/lockup/client/cli/query_test.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package cli_test
-
-import (
-	gocontext "context"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/suite"
-
-	"github.com/osmosis-labs/osmosis/v13/app/apptesting"
-	"github.com/osmosis-labs/osmosis/v13/x/lockup/types"
-
-	sdk "github.com/cosmos/cosmos-sdk/types"
-)
-
-type QueryTestSuite struct {
-	apptesting.KeeperTestHelper
-	queryClient types.QueryClient
-}
-
-func (s *QueryTestSuite) SetupSuite() {
-	s.Setup()
-	s.queryClient = types.NewQueryClient(s.QueryHelper)
-
-	// create a pool
-	s.PrepareBalancerPool()
-	// set up lock with id = 1
-	s.LockTokens(s.TestAccs[0], sdk.Coins{sdk.NewCoin("gamm/pool/1", sdk.NewInt(1000000))}, time.Hour*24)
-
-	s.Commit()
-}
-
-func (s *QueryTestSuite) TestQueriesNeverAlterState() {
-	testCases := []struct {
-		name   string
-		query  string
-		input  interface{}
-		output interface{}
-	}{
-		{
-			"Query account locked coins",
-			"/osmosis.lockup.Query/AccountLockedCoins",
-			&types.AccountLockedCoinsRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountLockedCoinsResponse{},
-		},
-		{
-			"Query account locked by duration",
-			"/osmosis.lockup.Query/AccountLockedDuration",
-			&types.AccountLockedDurationRequest{Owner: s.TestAccs[0].String(), Duration: time.Hour * 24},
-			&types.AccountLockedDurationResponse{},
-		},
-		{
-			"Query account locked longer than given duration",
-			"/osmosis.lockup.Query/AccountLockedLongerDuration",
-			&types.AccountLockedLongerDurationRequest{Owner: s.TestAccs[0].String(), Duration: time.Hour},
-			&types.AccountLockedLongerDurationResponse{},
-		},
-		{
-			"Query account locked by denom that longer than given duration",
-			"/osmosis.lockup.Query/AccountLockedLongerDurationDenom",
-			&types.AccountLockedLongerDurationDenomRequest{Owner: s.TestAccs[0].String(), Duration: time.Hour, Denom: "gamm/pool/1"},
-			&types.AccountLockedLongerDurationDenomResponse{},
-		},
-		{
-			"Query account locked longer than given duration not unlocking",
-			"/osmosis.lockup.Query/AccountLockedLongerDurationNotUnlockingOnly",
-			&types.AccountLockedLongerDurationNotUnlockingOnlyRequest{Owner: s.TestAccs[0].String(), Duration: time.Hour},
-			&types.AccountLockedLongerDurationNotUnlockingOnlyResponse{},
-		},
-		{
-			"Query account locked in past time",
-			"/osmosis.lockup.Query/AccountLockedPastTime",
-			&types.AccountLockedPastTimeRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountLockedPastTimeResponse{},
-		},
-		{
-			"Query account locked in past time by denom",
-			"/osmosis.lockup.Query/AccountLockedPastTimeDenom",
-			&types.AccountLockedPastTimeDenomRequest{Owner: s.TestAccs[0].String(), Denom: "gamm/pool/1"},
-			&types.AccountLockedPastTimeDenomResponse{},
-		},
-		{
-			" Query account locked in past time that not unlocking",
-			"/osmosis.lockup.Query/AccountLockedPastTimeNotUnlockingOnly",
-			&types.AccountLockedPastTimeNotUnlockingOnlyRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountLockedPastTimeNotUnlockingOnlyResponse{},
-		},
-		{
-			"Query account unlockable coins",
-			"/osmosis.lockup.Query/AccountUnlockableCoins",
-			&types.AccountUnlockableCoinsRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountUnlockableCoinsResponse{},
-		},
-		{
-			"Query account unlocked before given time",
-			"/osmosis.lockup.Query/AccountUnlockedBeforeTime",
-			&types.AccountUnlockedBeforeTimeRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountUnlockedBeforeTimeResponse{},
-		},
-		{
-			"Query account unlocking coins",
-			"/osmosis.lockup.Query/AccountUnlockingCoins",
-			&types.AccountUnlockingCoinsRequest{Owner: s.TestAccs[0].String()},
-			&types.AccountUnlockingCoinsResponse{},
-		},
-		{
-			"Query lock by id",
-			"/osmosis.lockup.Query/LockedByID",
-			&types.LockedRequest{LockId: 1},
-			&types.LockedResponse{},
-		},
-		{
-			"Query lock by denom",
-			"/osmosis.lockup.Query/LockedDenom",
-			&types.LockedDenomRequest{Duration: time.Hour * 24, Denom: "gamm/pool/1"},
-			&types.LockedDenomResponse{},
-		},
-		{
-			"Query module balances",
-			"/osmosis.lockup.Query/ModuleBalance",
-			&types.ModuleBalanceRequest{},
-			&types.ModuleBalanceResponse{},
-		},
-		{
-			"Query module locked amount",
-			"/osmosis.lockup.Query/ModuleLockedAmount",
-			&types.ModuleLockedAmountRequest{},
-			&types.ModuleLockedAmountResponse{},
-		},
-		{
-			"Query synthetic lock by id",
-			"/osmosis.lockup.Query/SyntheticLockupsByLockupID",
-			&types.SyntheticLockupsByLockupIDRequest{LockId: 1},
-			&types.SyntheticLockupsByLockupIDResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			s.SetupSuite()
-			err := s.QueryHelper.Invoke(gocontext.Background(), tc.query, tc.input, tc.output)
-			s.Require().NoError(err)
-			s.StateNotAltered()
-		})
-	}
-}
-
-func TestQueryTestSuite(t *testing.T) {
-	suite.Run(t, new(QueryTestSuite))
-}
diff --git a/x/lockup/client/testutil/test_helpers.go b/x/lockup/client/testutil/test_helpers.go
deleted file mode 100644
index 90a3d0f3f..000000000
--- a/x/lockup/client/testutil/test_helpers.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package testutil
-
-import (
-	"fmt"
-
-	lockupcli "github.com/osmosis-labs/osmosis/v13/x/lockup/client/cli"
-
-	"github.com/cosmos/cosmos-sdk/client"
-	"github.com/cosmos/cosmos-sdk/client/flags"
-	"github.com/cosmos/cosmos-sdk/testutil"
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	sdk "github.com/cosmos/cosmos-sdk/types"
-)
-
-// commonArgs is args for CLI test commands.
-var commonArgs = []string{
-	fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
-	fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
-	fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()),
-}
-
-// MsgLockTokens creates a lock tokens message.
-func MsgLockTokens(clientCtx client.Context, owner fmt.Stringer, amount fmt.Stringer, duration string, extraArgs ...string) (testutil.BufferWriter, error) {
-	args := []string{
-		amount.String(),
-		fmt.Sprintf("--%s=%s", lockupcli.FlagDuration, duration),
-		fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()),
-	}
-
-	args = append(args, commonArgs...)
-	return clitestutil.ExecTestCLICmd(clientCtx, lockupcli.NewLockTokensCmd(), args)
-}
-
-// MsgBeginUnlocking creates a begin unlock tokens message.
-func MsgBeginUnlocking(clientCtx client.Context, owner fmt.Stringer, extraArgs ...string) (testutil.BufferWriter, error) {
-	args := []string{
-		fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()),
-		fmt.Sprintf("--%s=%d", flags.FlagGas, 500000),
-	}
-
-	args = append(args, commonArgs...)
-	return clitestutil.ExecTestCLICmd(clientCtx, lockupcli.NewBeginUnlockingAllCmd(), args)
-}
diff --git a/x/pool-incentives/client/cli/cli_test.go b/x/pool-incentives/client/cli/cli_test.go
deleted file mode 100644
index d0333ba9d..000000000
--- a/x/pool-incentives/client/cli/cli_test.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package cli_test
-
-import (
-	"github.com/gogo/protobuf/proto"
-	"github.com/stretchr/testify/suite"
-
-	"github.com/osmosis-labs/osmosis/v13/app"
-	"github.com/osmosis-labs/osmosis/v13/x/pool-incentives/client/cli"
-	"github.com/osmosis-labs/osmosis/v13/x/pool-incentives/types"
-
-	clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
-	"github.com/cosmos/cosmos-sdk/testutil/network"
-)
-
-type IntegrationTestSuite struct {
-	suite.Suite
-
-	cfg     network.Config
-	network *network.Network
-}
-
-// SetupSuite sets up an integration test suite.
-func (s *IntegrationTestSuite) SetupSuite() {
-	s.T().Log("setting up integration test suite")
-
-	s.cfg = app.DefaultConfig()
-
-	s.network = network.New(s.T(), s.cfg)
-
-	_, err := s.network.WaitForHeight(1)
-	s.Require().NoError(err)
-}
-
-// TearDownSuite tears down the integration test suite.
-func (s *IntegrationTestSuite) TearDownSuite() {
-	s.T().Log("tearing down integration test suite")
-	s.network.Cleanup()
-}
-
-// TestGetCmdGaugeIds tests gaugeID CLI query commands
-func (s *IntegrationTestSuite) TestGetCmdGaugeIds() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query gauge-ids",
-			false, &types.QueryGaugeIdsResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdGaugeIds()
-			clientCtx := val.ClientCtx
-
-			args := []string{"1"}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
-
-// TestGetCmdDistrInfo tests distribution info CLI query commands
-func (s *IntegrationTestSuite) TestGetCmdDistrInfo() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query distr-info",
-			false, &types.QueryDistrInfoResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdDistrInfo()
-			clientCtx := val.ClientCtx
-
-			args := []string{}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
-
-// TestGetCmdLockableDurations tests lockable duration CLI query commands
-func (s *IntegrationTestSuite) TestGetCmdLockableDurations() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query lockable durations",
-			false, &types.QueryLockableDurationsResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdLockableDurations()
-			clientCtx := val.ClientCtx
-
-			args := []string{}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
-
-// TestGetCmdIncentivizedPools tests incentivized pools CLI query commands
-func (s *IntegrationTestSuite) TestGetCmdIncentivizedPools() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query incentivized pools",
-			false, &types.QueryIncentivizedPoolsResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdIncentivizedPools()
-			clientCtx := val.ClientCtx
-
-			args := []string{}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
-
-// TestGetCmdExternalIncentiveGauges tests incentivized pools CLI query commands
-func (s *IntegrationTestSuite) TestGetCmdExternalIncentiveGauges() {
-	val := s.network.Validators[0]
-
-	testCases := []struct {
-		name      string
-		expectErr bool
-		respType  proto.Message
-	}{
-		{
-			"query external incentivized pools",
-			false, &types.QueryExternalIncentiveGaugesResponse{},
-		},
-	}
-
-	for _, tc := range testCases {
-		tc := tc
-
-		s.Run(tc.name, func() {
-			cmd := cli.GetCmdExternalIncentiveGauges()
-			clientCtx := val.ClientCtx
-
-			args := []string{}
-
-			out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
-			if tc.expectErr {
-				s.Require().Error(err)
-			} else {
-				s.Require().NoError(err, out.String())
-				s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
-			}
-		})
-	}
-}
diff --git a/x/protorev/keeper/routes_test.go b/x/protorev/keeper/routes_test.go
new file mode 100644
index 000000000..8434643f0
--- /dev/null
+++ b/x/protorev/keeper/routes_test.go
@@ -0,0 +1,249 @@
+package keeper_test
+
+import (
+	"github.com/osmosis-labs/osmosis/v13/x/protorev/types"
+)
+
+type TestRoute struct {
+	PoolId      uint64
+	InputDenom  string
+	OutputDenom string
+}
+
+func (suite *KeeperTestSuite) TestBuildRoutes() {
+	cases := []struct {
+		description string
+		inputDenom  string
+		outputDenom string
+		poolID      uint64
+		expected    [][]TestRoute
+	}{
+		{
+			description: "Route exists for swap in Akash and swap out Atom",
+			inputDenom:  "akash",
+			outputDenom: types.AtomDenomination,
+			poolID:      1,
+			expected: [][]TestRoute{
+				{
+					{PoolId: 1, InputDenom: types.AtomDenomination, OutputDenom: "akash"},
+					{PoolId: 14, InputDenom: "akash", OutputDenom: "bitcoin"},
+					{PoolId: 4, InputDenom: "bitcoin", OutputDenom: types.AtomDenomination},
+				},
+				{
+					{PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: types.AtomDenomination},
+					{PoolId: 1, InputDenom: types.AtomDenomination, OutputDenom: "akash"},
+					{PoolId: 7, InputDenom: "akash", OutputDenom: types.OsmosisDenomination},
+				},
+			},
+		},
+		{
+			description: "Route exists for swap in Bitcoin and swap out Atom",
+			inputDenom:  "bitcoin",
+			outputDenom: types.AtomDenomination,
+			poolID:      4,
+			expected: [][]TestRoute{
+				{
+					{PoolId: 25, InputDenom: types.OsmosisDenomination, OutputDenom: types.AtomDenomination},
+					{PoolId: 4, InputDenom: types.AtomDenomination, OutputDenom: "bitcoin"},
+					{PoolId: 10, InputDenom: "bitcoin", OutputDenom: types.OsmosisDenomination},
+				},
+			},
+		},
+		{
+			description: "Route exists for swap in Bitcoin and swap out ethereum",
+			inputDenom:  "bitcoin",
+			outputDenom: "ethereum",
+			poolID:      19,
+			expected: [][]TestRoute{
+				{
+					{PoolId: 9, InputDenom: types.OsmosisDenomination, OutputDenom: "ethereum"},
+					{PoolId: 19, InputDenom: "ethereum", OutputDenom: "bitcoin"},
+					{PoolId: 10, InputDenom: "bitcoin", OutputDenom: types.OsmosisDenomination},
+				},
+				{
+					{PoolId: 3, InputDenom: types.AtomDenomination, OutputDenom: "ethereum"},
+					{PoolId: 19, InputDenom: "ethereum", OutputDenom: "bitcoin"},
+					{PoolId: 4, InputDenom: "bitcoin", OutputDenom: types.AtomDenomination},
+				},
+			},
+		},
+		{
+			description: "No route exists for swap in osmo and swap out Atom",
+			inputDenom:  types.OsmosisDenomination,
+			outputDenom: types.AtomDenomination,
+			poolID:      25,
+			expected:    [][]TestRoute{},
+		},
+	}
+
+	for _, tc := range cases {
+		suite.Run(tc.description, func() {
+			routes := suite.App.ProtoRevKeeper.BuildRoutes(suite.Ctx, tc.inputDenom, tc.outputDenom, tc.poolID)
+
+			suite.Require().Equal(len(tc.expected), len(routes))
+
+			for routeIndex, route := range routes {
+				for tradeIndex, trade := range route.Trades {
+					suite.Require().Equal(tc.expected[routeIndex][tradeIndex].PoolId, trade.Pool.GetId())
+					suite.Require().Equal(tc.expected[routeIndex][tradeIndex].InputDenom, trade.InputDenom)
+					suite.Require().Equal(tc.expected[routeIndex][tradeIndex].OutputDenom, trade.OutputDenom)
+				}
+			}
+		})
+	}
+}
+
+func (suite *KeeperTestSuite) TestBuildAtomRoute() {
+	cases := []struct {
+		description   string
+		swapIn        string
+		swapOut       string
+		poolId        uint64
+		expectedRoute []TestRoute
+		hasRoute      bool
+	}{
+		{
+			description:   "Route exists for swap in Osmo and swap out Akash",
+			swapIn:        types.OsmosisDenomination,
+			swapOut:       "akash",
+			poolId:        7,
+			expectedRoute: []TestRoute{{1, types.AtomDenomination, "akash"}, {7, "akash", types.OsmosisDenomination}, {25, types.OsmosisDenomination, types.AtomDenomination}},
+			hasRoute:      true,
+		},
+		{
+			description:   "Route exists for swap in Akash and swap out Osmo",
+			swapIn:        "akash",
+			swapOut:       types.OsmosisDenomination,
+			poolId:        7,
+			expectedRoute: []TestRoute{{25, types.AtomDenomination, types.OsmosisDenomination}, {7, types.OsmosisDenomination, "akash"}, {1, "akash", types.AtomDenomination}},
+			hasRoute:      true,
+		},
+		{
+			description:   "Route does not exist for swap in Terra and swap out Osmo because the pool does not exist",
+			swapIn:        "terra",
+			swapOut:       types.OsmosisDenomination,
+			poolId:        7,
+			expectedRoute: []TestRoute{},
+			hasRoute:      false,
+		},
+	}
+
+	for _, tc := range cases {
+		suite.Run(tc.description, func() {
+			route, err := suite.App.ProtoRevKeeper.BuildAtomRoute(suite.Ctx, tc.swapIn, tc.swapOut, tc.poolId)
+
+			if tc.hasRoute {
+				suite.Require().NoError(err)
+				suite.Require().Equal(len(tc.expectedRoute), len(route.Trades))
+
+				for index, trade := range tc.expectedRoute {
+					suite.Require().Equal(trade.PoolId, route.Trades[index].Pool.GetId())
+					suite.Require().Equal(trade.InputDenom, route.Trades[index].InputDenom)
+					suite.Require().Equal(trade.OutputDenom, route.Trades[index].OutputDenom)
+				}
+			} else {
+				suite.Require().Error(err)
+			}
+		})
+	}
+}
+
+func (suite *KeeperTestSuite) TestBuildOsmoRoute() {
+	cases := []struct {
+		description   string
+		swapIn        string
+		swapOut       string
+		poolId        uint64
+		expectedRoute []TestRoute
+		hasRoute      bool
+	}{
+		{
+			description:   "Route exists for swap in Atom and swap out Akash",
+			swapIn:        types.AtomDenomination,
+			swapOut:       "akash",
+			poolId:        1,
+			expectedRoute: []TestRoute{{7, types.OsmosisDenomination, "akash"}, {1, "akash", types.AtomDenomination}, {25, types.AtomDenomination, types.OsmosisDenomination}},
+			hasRoute:      true,
+		},
+		{
+			description:   "Route exists for swap in Akash and swap out Atom",
+			swapIn:        "akash",
+			swapOut:       types.AtomDenomination,
+			poolId:        1,
+			expectedRoute: []TestRoute{{25, types.OsmosisDenomination, types.AtomDenomination}, {1, types.AtomDenomination, "akash"}, {7, "akash", types.OsmosisDenomination}},
+			hasRoute:      true,
+		},
+		{
+			description:   "Route does not exist for swap in Terra and swap out Atom because the pool does not exist",
+			swapIn:        "terra",
+			swapOut:       types.AtomDenomination,
+			poolId:        7,
+			expectedRoute: []TestRoute{},
+			hasRoute:      false,
+		},
+	}
+
+	for _, tc := range cases {
+		suite.Run(tc.description, func() {
+			route, err := suite.App.ProtoRevKeeper.BuildOsmoRoute(suite.Ctx, tc.swapIn, tc.swapOut, tc.poolId)
+
+			if tc.hasRoute {
+				suite.Require().NoError(err)
+				suite.Require().Equal(len(tc.expectedRoute), len(route.Trades))
+
+				for index, trade := range tc.expectedRoute {
+					suite.Require().Equal(trade.PoolId, route.Trades[index].Pool.GetId())
+					suite.Require().Equal(trade.InputDenom, route.Trades[index].InputDenom)
+					suite.Require().Equal(trade.OutputDenom, route.Trades[index].OutputDenom)
+				}
+			} else {
+				suite.Require().Error(err)
+			}
+		})
+	}
+}
+
+func (suite *KeeperTestSuite) TestBuildTokenPairRoutes() {
+	cases := []struct {
+		description    string
+		swapIn         string
+		swapOut        string
+		poolId         uint64
+		expectedRoutes [][]TestRoute
+		hasRoutes      bool
+	}{
+		{
+			description:    "Route exists for swap in Atom and swap out Akash",
+			swapIn:         "akash",
+			swapOut:        types.AtomDenomination,
+			poolId:         1,
+			expectedRoutes: [][]TestRoute{{{1, types.AtomDenomination, "akash"}, {14, "akash", "bitcoin"}, {4, "bitcoin", types.AtomDenomination}}},
+			hasRoutes:      true,
+		},
+	}
+
+	for _, tc := range cases {
+		suite.Run(tc.description, func() {
+			routes, err := suite.App.ProtoRevKeeper.BuildTokenPairRoutes(suite.Ctx, tc.swapIn, tc.swapOut, tc.poolId)
+
+			if tc.hasRoutes {
+				suite.Require().NoError(err)
+				suite.Require().Equal(len(tc.expectedRoutes), len(routes))
+
+				for index, route := range routes {
+
+					suite.Require().Equal(len(tc.expectedRoutes[index]), len(route.Trades))
+
+					for index, trade := range tc.expectedRoutes[index] {
+						suite.Require().Equal(trade.PoolId, route.Trades[index].Pool.GetId())
+						suite.Require().Equal(trade.InputDenom, route.Trades[index].InputDenom)
+						suite.Require().Equal(trade.OutputDenom, route.Trades[index].OutputDenom)
+					}
+				}
+
+			} else {
+				suite.Require().Error(err)
+			}
+		})
+	}
+}