Swap module scaffolding and params (#922)

* swap module scaffolding

* global swap fee

* can't think of a reason for begin blocker; removing for abci.go for now;

* test pair types; refactor pair name logic; simplify pairs validation and
fix stack overflow error

* check comparison

* use test package

* init swap module genesis

* add basic marshall tests

* remove reward apy from pairs

* fix integration helpers

* use max swap fee constant; fix validation of swap fee; add tests to
cover param validation and param set setup

* use noerror over nil

* start genesis tests

* test param set validation mirrors param validation

* add genesis tests

* remove print statement

* add subtests for genesis test cases; add extra querier test for unknown
route; add keeper params testing

* add spec

* update swagger

* find replace hard -> swap in comments

* remove unused method

* rename pairs to allowed pools; pool is more commonly used, and
allowedPool makes it more clear what swap parameter is for.  In
addition, we won't conflict with Pool data structure for storing a
created pool in the store.

* remove generated link

* missed spec rename

* validate token order for allowed pools

* fix swagger

* json should be snakecase; change allowedPools to allowed_pools

Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>
This commit is contained in:
Denali Marsh 2021-06-08 17:19:12 +02:00 committed by GitHub
parent cae7503f7b
commit 9d9b169e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1634 additions and 3 deletions

View File

@ -43,6 +43,7 @@ import (
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
"github.com/kava-labs/kava/x/swap"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
@ -85,6 +86,7 @@ var (
incentive.AppModuleBasic{},
issuance.AppModuleBasic{},
hard.AppModuleBasic{},
swap.AppModuleBasic{},
)
// module account permissions
@ -158,6 +160,7 @@ type App struct {
incentiveKeeper incentive.Keeper
issuanceKeeper issuance.Keeper
hardKeeper hard.Keeper
swapKeeper swap.Keeper
// the module manager
mm *module.Manager
@ -181,7 +184,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
hard.StoreKey,
hard.StoreKey, swap.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -212,6 +215,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
hardSubspace := app.paramsKeeper.Subspace(hard.DefaultParamspace)
swapSubspace := app.paramsKeeper.Subspace(swap.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -393,6 +397,11 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
app.accountKeeper,
app.supplyKeeper,
)
app.swapKeeper = swap.NewKeeper(
app.cdc,
keys[swap.StoreKey],
swapSubspace,
)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -428,6 +437,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper),
swap.NewAppModule(app.swapKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
@ -448,7 +458,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, hard.ModuleName, auction.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, hard.ModuleName, auction.ModuleName, swap.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
crisis.ModuleName, // runs the invariants at genesis - should run after other modules

View File

@ -38,6 +38,7 @@ import (
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
"github.com/kava-labs/kava/x/swap"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
@ -87,6 +88,7 @@ func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiv
func (tApp TestApp) GetHardKeeper() hard.Keeper { return tApp.hardKeeper }
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
func (tApp TestApp) GetSwapKeeper() swap.Keeper { return tApp.swapKeeper }
// InitializeFromGenesisStates calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {

View File

@ -2409,7 +2409,29 @@ paths:
$ref: "#/definitions/Coin"
500:
description: Server internal error
/swap/parameters:
get:
summary: Get the current parameters of the swap module
tags:
- Swap
produces:
- application/json
responses:
200:
description: Swap module parameters
schema:
type: object
properties:
height:
type: string
example: "100"
result:
type: array
x-nullable: true
items:
$ref: "#/definitions/SwapParams"
500:
description: Server internal error
/bank/accounts/{address}/transfers:
post:
summary: Send coins from one account to another
@ -4837,6 +4859,25 @@ definitions:
active:
type: boolean
example: true
SwapParams:
type: object
properties:
allowed_pools:
type: array
items:
$ref: "#/definitions/AllowedPool"
swap_fee:
type: string
example: "0.03"
AllowedPool:
type: object
properties:
token_a:
type: string
example: "ukava"
token_b:
type: string
example: "usdx"
HardParams:
type: object
properties:

28
x/swap/alias.go Normal file
View File

@ -0,0 +1,28 @@
package swap
import (
"github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/types"
)
const (
ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
RouterKey = types.RouterKey
StoreKey = types.StoreKey
DefaultParamspace = types.DefaultParamspace
)
type (
GenesisState = types.GenesisState
Keeper = keeper.Keeper
)
var (
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
ModuleCdc = types.ModuleCdc
ParamKeyTable = types.ParamKeyTable
RegisterCodec = types.RegisterCodec
DefaultGenesisState = types.DefaultGenesisState
)

View File

@ -0,0 +1,58 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/swap/types"
)
// GetQueryCmd returns the cli query commands for the module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
swapQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the swap module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
swapQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
)...)
return swapQueryCmd
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the swap module parameters",
Long: "Get the current global swap module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var params types.Params
if err := cdc.UnmarshalJSON(res, &params); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
return cliCtx.PrintOutput(params)
},
}
}

28
x/swap/client/cli/tx.go Normal file
View File

@ -0,0 +1,28 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/swap/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
swapTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("%s transactions subcommands", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
swapTxCmd.AddCommand(flags.PostCommands()...)
return swapTxCmd
}

View File

@ -0,0 +1,37 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/swap/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
}
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryGetParams)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,17 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// REST variable names
// nolint
const ()
// RegisterRoutes registers swap-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

9
x/swap/client/rest/tx.go Normal file
View File

@ -0,0 +1,9 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {}

24
x/swap/genesis.go Normal file
View File

@ -0,0 +1,24 @@
package swap
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/swap/types"
)
// InitGenesis initializes story state from genesis file
func InitGenesis(ctx sdk.Context, k Keeper, gs types.GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
k.SetParams(ctx, gs.Params)
}
// ExportGenesis exports the genesis state
func ExportGenesis(ctx sdk.Context, k Keeper) types.GenesisState {
params := k.GetParams(ctx)
return types.NewGenesisState(params)
}

17
x/swap/handler.go Normal file
View File

@ -0,0 +1,17 @@
package swap
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// NewHandler creates an sdk.Handler for swap messages
func NewHandler(k Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}

View File

@ -0,0 +1,32 @@
package keeper_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap/types"
)
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func NewAuthGenStateFromAccs(accounts ...authexported.GenesisAccount) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}
func NewSwapGenStateMulti() app.GenesisState {
swapGenesis := types.GenesisState{
Params: types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("ukava", "usdx"),
},
SwapFee: sdk.MustNewDecFromStr("0.03"),
},
}
return app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(swapGenesis)}
}

29
x/swap/keeper/keeper.go Normal file
View File

@ -0,0 +1,29 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/swap/types"
)
// Keeper keeper for the swap module
type Keeper struct {
key sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
}
// NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace) Keeper {
if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
key: key,
cdc: cdc,
paramSubspace: paramstore,
}
}

19
x/swap/keeper/params.go Normal file
View File

@ -0,0 +1,19 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/swap/types"
)
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.Params
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}

View File

@ -0,0 +1,39 @@
package keeper_test
import (
"testing"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func TestParams_SetterAndGetter(t *testing.T) {
tApp := app.NewTestApp()
keeper := tApp.GetSwapKeeper()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
params := types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("ukava", "usdx"),
},
SwapFee: sdk.MustNewDecFromStr("0.03"),
}
keeper.SetParams(ctx, params)
assert.Equal(t, keeper.GetParams(ctx), params)
oldParams := params
params = types.Params{
AllowedPools: types.AllowedPools{
types.NewAllowedPool("hard", "ukava"),
},
SwapFee: sdk.MustNewDecFromStr("0.01"),
}
keeper.SetParams(ctx, params)
assert.NotEqual(t, keeper.GetParams(ctx), oldParams)
assert.Equal(t, keeper.GetParams(ctx), params)
}

36
x/swap/keeper/querier.go Normal file
View File

@ -0,0 +1,36 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/swap/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(k Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetParams:
return queryGetParams(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
// query params in the swap store
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get params
params := keeper.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -0,0 +1,65 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/types"
)
type QuerierTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
querier sdk.Querier
}
func (suite *QuerierTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates(
NewSwapGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetSwapKeeper()
suite.querier = keeper.NewQuerier(suite.keeper)
}
func (suite *QuerierTestSuite) TestUnkownRequest() {
ctx := suite.ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{"invalid-path"}, abci.RequestQuery{})
suite.Nil(bz)
suite.EqualError(err, "unknown request: unknown swap query endpoint")
}
func (suite *QuerierTestSuite) TestQueryParams() {
ctx := suite.ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Nil(err)
suite.NotNil(bz)
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
swapGenesisState := NewSwapGenStateMulti()
gs := types.GenesisState{}
types.ModuleCdc.UnmarshalJSON(swapGenesisState["swap"], &gs)
suite.Equal(gs.Params, p)
}
func TestQuerierTestSuite(t *testing.T) {
suite.Run(t, new(QuerierTestSuite))
}

166
x/swap/module.go Normal file
View File

@ -0,0 +1,166 @@
package swap
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/tendermint/tendermint/abci/types"
// "github.com/kava-labs/kava/x/swap/simulation"
"github.com/kava-labs/kava/x/swap/client/cli"
"github.com/kava-labs/kava/x/swap/client/rest"
"github.com/kava-labs/kava/x/swap/keeper"
"github.com/kava-labs/kava/x/swap/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
// _ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers REST routes for the swap module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the swap module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns no root query command for the swap module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(types.StoreKey, cdc)
}
//____________________________________________________________________________
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return QuerierRoute
}
// NewQuerierHandler returns no sdk.Querier.
func (am AppModule) NewQuerierHandler() sdk.Querier {
return keeper.NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {
}
// EndBlock module end-block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
//____________________________________________________________________________
// // GenerateGenesisState creates a randomized GenState of the swap module
// func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
// simulation.RandomizedGenState(simState)
// }
// // ProposalContents doesn't return any content functions for governance proposals.
// func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
// return nil
// }
// // RandomizedParams returns nil because swap has no params.
// func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
// return simulation.ParamChanges(r)
// }
// // RegisterStoreDecoder registers a decoder for swap module's types
// func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
// sdr[StoreKey] = simulation.DecodeStore
// }
// // WeightedOperations returns the all the swap module operations with their respective weights.
// func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
// return nil
// }

View File

@ -0,0 +1,13 @@
<!--
order: 1
-->
# Concepts
## Automated Market Maker
The swap module provides for functionality and governance of an Automated Market Maker protocol. The main state transitions in the swap module include deposits/withdrawals to liquidity pools by liquidity providers and token swaps executed against liquidity pools by users. Each liquidity pool consists of a unique pair of two tokens. A global swap fee set by governance is paid by users to execute trades, with the proceeds going to the relevant pool's liquidity providers.
## SWP Token distribution
[See Incentive Module](../../incentive/spec/01_concepts.md)

35
x/swap/spec/02_state.md Normal file
View File

@ -0,0 +1,35 @@
<!--
order: 2
-->
# State
## Parameters and Genesis State
`Parameters` define the governance parameters and default behavior of the swap module.
```go
// Params are governance parameters for the swap module
type Params struct {
AllowedPools AllowedPools `json:"allowed_pools" yaml:"allowed_pools"`
SwapFee sdk.Dec `json:"swap_fee" yaml:"swap_fee"`
}
// AllowedPool defines a tradable pool
type AllowedPool struct {
TokenA string `json:"token_a" yaml:"token_a"`
TokenB string `json:"token_b" yaml:"token_b"`
}
// AllowedPools is a slice of AllowedPool
type AllowedPools []AllowedPool
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for the normal function of the swap module to resume.
```go
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
}
```

View File

@ -0,0 +1,5 @@
<!--
order: 3
-->
# Messages

9
x/swap/spec/04_events.md Normal file
View File

@ -0,0 +1,9 @@
<!--
order: 4
-->
# Events
The swap module emits the following events:
## Handlers

19
x/swap/spec/05_params.md Normal file
View File

@ -0,0 +1,19 @@
<!--
order: 5
-->
# Parameters
Example parameters for the swap module:
| Key | Type | Example | Description |
| ------------ | ------------------- | ------------- | --------------------------------------- |
| AllowedPools | array (AllowedPool) | [{see below}] | Array of tradable pools supported |
| SwapFee | sdk.Dec | 0.03 | Global trading fee in percentage format |
Example parameters for `AllowedPool`:
| Key | Type | Example | Description |
| ------ | ------ | ------- | ------------------- |
| TokenA | string | "ukava" | First coin's denom |
| TokenB | string | "usdx" | Second coin's denom |

20
x/swap/spec/README.md Normal file
View File

@ -0,0 +1,20 @@
<!--
order: 0
title: "Swap Overview"
parent:
title: "swap"
-->
# `swap`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
## Abstract
`x/swap` is a Cosmos SDK module that implements an Automated Market Maker (AMM) that enables users to swap coins by trading against liquidity pools.

18
x/swap/types/codec.go Normal file
View File

@ -0,0 +1,18 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc generic sealed codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers the necessary types for swap module
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(AllowedPool{}, "swap/AllowedPool", nil)
}

11
x/swap/types/errors.go Normal file
View File

@ -0,0 +1,11 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DONTCOVER
var (
ErrCustom = sdkerrors.Register(ModuleName, 2, "")
)

6
x/swap/types/events.go Normal file
View File

@ -0,0 +1,6 @@
package types
// Event types for swap module
const (
// EventTypeCustom = ""
)

42
x/swap/types/genesis.go Normal file
View File

@ -0,0 +1,42 @@
package types
import "bytes"
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
}
// NewGenesisState creates a new genesis state.
func NewGenesisState(params Params) GenesisState {
return GenesisState{
Params: params,
}
}
// Validate validates the module's genesis state
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
return nil
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultParams(),
)
}
// Equal checks whether two gov GenesisState structs are equivalent
func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}

View File

@ -0,0 +1,168 @@
package types_test
import (
"testing"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenesis_Default(t *testing.T) {
defaultGenesis := types.DefaultGenesisState()
require.NoError(t, defaultGenesis.Validate())
defaultParams := types.DefaultParams()
assert.Equal(t, defaultParams, defaultGenesis.Params)
}
func TestGenesis_Empty(t *testing.T) {
emptyGenesis := types.GenesisState{}
assert.True(t, emptyGenesis.IsEmpty())
emptyGenesis = types.GenesisState{
Params: types.Params{},
}
assert.True(t, emptyGenesis.IsEmpty())
}
func TestGenesis_NotEmpty(t *testing.T) {
nonEmptyGenesis := types.GenesisState{
Params: types.Params{
AllowedPools: types.NewAllowedPools(types.NewAllowedPool("ukava", "hard")),
SwapFee: sdk.ZeroDec(),
},
}
assert.False(t, nonEmptyGenesis.IsEmpty())
}
func TestGenesis_Validate_SwapFee(t *testing.T) {
type args struct {
name string
swapFee sdk.Dec
expectErr bool
}
// More comprehensive swap fee tests are in prams_test.go
testCases := []args{
{
"normal",
sdk.MustNewDecFromStr("0.25"),
false,
},
{
"negative",
sdk.MustNewDecFromStr("-0.5"),
true,
},
{
"greater than 1.0",
sdk.MustNewDecFromStr("1.001"),
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
genesisState := types.GenesisState{
Params: types.Params{
AllowedPools: types.DefaultAllowedPools,
SwapFee: tc.swapFee,
},
}
err := genesisState.Validate()
if tc.expectErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestGenesis_Validate_AllowedPools(t *testing.T) {
type args struct {
name string
pairs types.AllowedPools
expectErr bool
}
// More comprehensive pair validation tests are in pair_test.go, params_test.go
testCases := []args{
{
"normal",
types.DefaultAllowedPools,
false,
},
{
"invalid",
types.AllowedPools{
{
TokenA: "same",
TokenB: "same",
},
},
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
genesisState := types.GenesisState{
Params: types.Params{
AllowedPools: tc.pairs,
SwapFee: types.DefaultSwapFee,
},
}
err := genesisState.Validate()
if tc.expectErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestGenesis_Equal(t *testing.T) {
params := types.Params{
types.NewAllowedPools(types.NewAllowedPool("ukava", "usdx")),
sdk.MustNewDecFromStr("0.85"),
}
genesisA := types.GenesisState{params}
genesisB := types.GenesisState{params}
assert.True(t, genesisA.Equal(genesisB))
}
func TestGenesis_NotEqual(t *testing.T) {
baseParams := types.Params{
types.NewAllowedPools(types.NewAllowedPool("ukava", "usdx")),
sdk.MustNewDecFromStr("0.85"),
}
// Base params
genesisAParams := baseParams
genesisA := types.GenesisState{genesisAParams}
// Different swap fee
genesisBParams := baseParams
genesisBParams.SwapFee = sdk.MustNewDecFromStr("0.84")
genesisB := types.GenesisState{genesisBParams}
// Different pairs
genesisCParams := baseParams
genesisCParams.AllowedPools = types.NewAllowedPools(types.NewAllowedPool("ukava", "hard"))
genesisC := types.GenesisState{genesisCParams}
// A and B have different swap fees
assert.False(t, genesisA.Equal(genesisB))
// A and C have different pair token B denoms
assert.False(t, genesisA.Equal(genesisC))
// A and B and different swap fees and pair token B denoms
assert.False(t, genesisA.Equal(genesisB))
}

22
x/swap/types/keys.go Normal file
View File

@ -0,0 +1,22 @@
package types
const (
// ModuleName name that will be used throughout the module
ModuleName = "swap"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// QuerierRoute Top level query string
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)
var (
// KeyPrefix = []byte{0x01}
)

1
x/swap/types/msg.go Normal file
View File

@ -0,0 +1 @@
package types

91
x/swap/types/params.go Normal file
View File

@ -0,0 +1,91 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
)
// Parameter keys and default values
var (
KeyAllowedPools = []byte("AllowedPools")
KeySwapFee = []byte("SwapFee")
DefaultAllowedPools = AllowedPools{}
DefaultSwapFee = sdk.ZeroDec()
MaxSwapFee = sdk.OneDec()
)
// Params are governance parameters for the swap module
type Params struct {
AllowedPools AllowedPools `json:"allowed_pools" yaml:"allowed_pools"`
SwapFee sdk.Dec `json:"swap_fee" yaml:"swap_fee"`
}
// NewParams returns a new params object
func NewParams(pairs AllowedPools, swapFee sdk.Dec) Params {
return Params{
AllowedPools: pairs,
SwapFee: swapFee,
}
}
// DefaultParams returns default params for swap module
func DefaultParams() Params {
return NewParams(
DefaultAllowedPools,
DefaultSwapFee,
)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
AllowedPools: %s
SwapFee: %s`,
p.AllowedPools, p.SwapFee)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetAllowedPools implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyAllowedPools, &p.AllowedPools, validateAllowedPoolsParams),
params.NewParamSetPair(KeySwapFee, &p.SwapFee, validateSwapFee),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateAllowedPoolsParams(p.AllowedPools); err != nil {
return err
}
return validateSwapFee(p.SwapFee)
}
func validateAllowedPoolsParams(i interface{}) error {
p, ok := i.(AllowedPools)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return p.Validate()
}
func validateSwapFee(i interface{}) error {
swapFee, ok := i.(sdk.Dec)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if swapFee.IsNil() || swapFee.IsNegative() || swapFee.GT(MaxSwapFee) {
return fmt.Errorf(fmt.Sprintf("invalid swap fee: %s", swapFee))
}
return nil
}

233
x/swap/types/params_test.go Normal file
View File

@ -0,0 +1,233 @@
package types_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/kava-labs/kava/x/swap/types"
sdk "github.com/cosmos/cosmos-sdk/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
func TestParams_UnmarshalJSON(t *testing.T) {
pools := types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("hard", "usdx"),
)
poolData, err := json.Marshal(pools)
require.NoError(t, err)
fee, err := sdk.NewDecFromStr("0.5")
require.NoError(t, err)
feeData, err := json.Marshal(fee)
require.NoError(t, err)
data := fmt.Sprintf(`{
"allowed_pools": %s,
"swap_fee": %s
}`, string(poolData), string(feeData))
var params types.Params
err = json.Unmarshal([]byte(data), &params)
require.NoError(t, err)
assert.Equal(t, pools, params.AllowedPools)
assert.Equal(t, fee, params.SwapFee)
}
func TestParams_MarshalYAML(t *testing.T) {
pools := types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("hard", "usdx"),
)
fee, err := sdk.NewDecFromStr("0.5")
require.NoError(t, err)
p := types.Params{
AllowedPools: pools,
SwapFee: fee,
}
data, err := yaml.Marshal(p)
require.NoError(t, err)
var params map[string]interface{}
err = yaml.Unmarshal(data, &params)
require.NoError(t, err)
_, ok := params["allowed_pools"]
require.True(t, ok)
_, ok = params["swap_fee"]
require.True(t, ok)
}
func TestParams_Default(t *testing.T) {
defaultParams := types.DefaultParams()
require.NoError(t, defaultParams.Validate())
assert.Equal(t, types.DefaultAllowedPools, defaultParams.AllowedPools)
assert.Equal(t, types.DefaultSwapFee, defaultParams.SwapFee)
assert.Equal(t, 0, len(defaultParams.AllowedPools))
assert.Equal(t, sdk.ZeroDec(), defaultParams.SwapFee)
}
func TestParams_ParamSetPairs_AllowedPools(t *testing.T) {
assert.Equal(t, []byte("AllowedPools"), types.KeyAllowedPools)
defaultParams := types.DefaultParams()
var paramSetPair *paramstypes.ParamSetPair
for _, pair := range defaultParams.ParamSetPairs() {
if bytes.Compare(pair.Key, types.KeyAllowedPools) == 0 {
paramSetPair = &pair
break
}
}
require.NotNil(t, paramSetPair)
pairs, ok := paramSetPair.Value.(*types.AllowedPools)
require.True(t, ok)
assert.Equal(t, pairs, &defaultParams.AllowedPools)
assert.Nil(t, paramSetPair.ValidatorFn(*pairs))
assert.EqualError(t, paramSetPair.ValidatorFn(struct{}{}), "invalid parameter type: struct {}")
}
func TestParams_ParamSetPairs_SwapFee(t *testing.T) {
assert.Equal(t, []byte("SwapFee"), types.KeySwapFee)
defaultParams := types.DefaultParams()
var paramSetPair *paramstypes.ParamSetPair
for _, pair := range defaultParams.ParamSetPairs() {
if bytes.Compare(pair.Key, types.KeySwapFee) == 0 {
paramSetPair = &pair
break
}
}
require.NotNil(t, paramSetPair)
swapFee, ok := paramSetPair.Value.(*sdk.Dec)
require.True(t, ok)
assert.Equal(t, swapFee, &defaultParams.SwapFee)
assert.Nil(t, paramSetPair.ValidatorFn(*swapFee))
assert.EqualError(t, paramSetPair.ValidatorFn(struct{}{}), "invalid parameter type: struct {}")
}
func TestParams_Validation(t *testing.T) {
testCases := []struct {
name string
key []byte
testFn func(params *types.Params)
expectedErr string
}{
{
name: "invalid denom",
key: types.KeyAllowedPools,
testFn: func(params *types.Params) {
params.AllowedPools = types.NewAllowedPools(types.NewAllowedPool("UKAVA", "ukava"))
},
expectedErr: "invalid denom: UKAVA",
},
{
name: "duplicate pools",
key: types.KeyAllowedPools,
testFn: func(params *types.Params) {
params.AllowedPools = types.NewAllowedPools(types.NewAllowedPool("ukava", "ukava"))
},
expectedErr: "pool cannot have two tokens of the same type, received 'ukava' and 'ukava'",
},
{
name: "nil swap fee",
key: types.KeySwapFee,
testFn: func(params *types.Params) {
params.SwapFee = sdk.Dec{}
},
expectedErr: "invalid swap fee: <nil>",
},
{
name: "negative swap fee",
key: types.KeySwapFee,
testFn: func(params *types.Params) {
params.SwapFee = sdk.NewDec(-1)
},
expectedErr: "invalid swap fee: -1.000000000000000000",
},
{
name: "swap fee greater than 1",
key: types.KeySwapFee,
testFn: func(params *types.Params) {
params.SwapFee = sdk.MustNewDecFromStr("1.000000000000000001")
},
expectedErr: "invalid swap fee: 1.000000000000000001",
},
{
name: "0 swap fee",
key: types.KeySwapFee,
testFn: func(params *types.Params) {
params.SwapFee = sdk.ZeroDec()
},
expectedErr: "",
},
{
name: "1 swap fee",
key: types.KeySwapFee,
testFn: func(params *types.Params) {
params.SwapFee = sdk.OneDec()
},
expectedErr: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
params := types.DefaultParams()
tc.testFn(&params)
err := params.Validate()
if tc.expectedErr == "" {
assert.Nil(t, err)
} else {
assert.EqualError(t, err, tc.expectedErr)
}
var paramSetPair *paramstypes.ParamSetPair
for _, pair := range params.ParamSetPairs() {
if bytes.Compare(pair.Key, tc.key) == 0 {
paramSetPair = &pair
break
}
}
require.NotNil(t, paramSetPair)
value := reflect.ValueOf(paramSetPair.Value).Elem().Interface()
// assert validation error is same as param set validation
assert.Equal(t, err, paramSetPair.ValidatorFn(value))
})
}
}
func TestParams_String(t *testing.T) {
params := types.NewParams(
types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("ukava", "usdx"),
),
sdk.MustNewDecFromStr("0.5"),
)
require.NoError(t, params.Validate())
output := params.String()
assert.Contains(t, output, "hard/ukava")
assert.Contains(t, output, "ukava/usdx")
assert.Contains(t, output, "0.5")
}

90
x/swap/types/pool.go Normal file
View File

@ -0,0 +1,90 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// AllowedPool defines a tradable pool
type AllowedPool struct {
TokenA string `json:"token_a" yaml:"token_a"`
TokenB string `json:"token_b" yaml:"token_b"`
}
// NewAllowedPool returns a new AllowedPool object
func NewAllowedPool(tokenA, tokenB string) AllowedPool {
return AllowedPool{
TokenA: tokenA,
TokenB: tokenB,
}
}
// Validate validates allowedPool attributes and returns an error if invalid
func (p AllowedPool) Validate() error {
err := sdk.ValidateDenom(p.TokenA)
if err != nil {
return err
}
err = sdk.ValidateDenom(p.TokenB)
if err != nil {
return err
}
if p.TokenA == p.TokenB {
return fmt.Errorf(
"pool cannot have two tokens of the same type, received '%s' and '%s'",
p.TokenA, p.TokenB,
)
}
if p.TokenA > p.TokenB {
return fmt.Errorf(
"invalid token order: '%s' must come before '%s'",
p.TokenB, p.TokenA,
)
}
return nil
}
// Name returns a unique name for a allowedPool in alphabetical order
func (p AllowedPool) Name() string {
return fmt.Sprintf("%s/%s", p.TokenA, p.TokenB)
}
// String pretty prints the allowedPool
func (p AllowedPool) String() string {
return fmt.Sprintf(`AllowedPool:
Name: %s
Token A: %s
Token B: %s
`, p.Name(), p.TokenA, p.TokenB)
}
// AllowedPools is a slice of AllowedPool
type AllowedPools []AllowedPool
// NewAllowedPools returns AllowedPools from the provided values
func NewAllowedPools(allowedPools ...AllowedPool) AllowedPools {
return AllowedPools(allowedPools)
}
// Validate validates each allowedPool and returns an error if there are any duplicates
func (p AllowedPools) Validate() error {
seenAllowedPools := make(map[string]bool)
for _, allowedPool := range p {
err := allowedPool.Validate()
if err != nil {
return err
}
if seen := seenAllowedPools[allowedPool.Name()]; seen {
return fmt.Errorf("duplicate pool: %s", allowedPool.Name())
}
seenAllowedPools[allowedPool.Name()] = true
}
return nil
}

185
x/swap/types/pool_test.go Normal file
View File

@ -0,0 +1,185 @@
package types_test
import (
"strings"
"testing"
types "github.com/kava-labs/kava/x/swap/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAllowedPool_Validation(t *testing.T) {
testCases := []struct {
name string
allowedPool types.AllowedPool
expectedErr string
}{
{
name: "blank token a",
allowedPool: types.NewAllowedPool("", "ukava"),
expectedErr: "invalid denom: ",
},
{
name: "blank token b",
allowedPool: types.NewAllowedPool("ukava", ""),
expectedErr: "invalid denom: ",
},
{
name: "invalid token a",
allowedPool: types.NewAllowedPool("1ukava", "ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "invalid token b",
allowedPool: types.NewAllowedPool("ukava", "1ukava"),
expectedErr: "invalid denom: 1ukava",
},
{
name: "no uppercase letters token a",
allowedPool: types.NewAllowedPool("uKava", "ukava"),
expectedErr: "invalid denom: uKava",
},
{
name: "no uppercase letters token b",
allowedPool: types.NewAllowedPool("ukava", "UKAVA"),
expectedErr: "invalid denom: UKAVA",
},
{
name: "matching tokens",
allowedPool: types.NewAllowedPool("ukava", "ukava"),
expectedErr: "pool cannot have two tokens of the same type, received 'ukava' and 'ukava'",
},
{
name: "invalid token order",
allowedPool: types.NewAllowedPool("usdx", "ukava"),
expectedErr: "invalid token order: 'ukava' must come before 'usdx'",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPool.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}
// ensure no regression in case insentive token matching if
// sdk.ValidateDenom ever allows upper case letters
func TestAllowedPool_TokenMatch(t *testing.T) {
allowedPool := types.NewAllowedPool("UKAVA", "ukava")
err := allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("hard", "haRd")
err = allowedPool.Validate()
assert.Error(t, err)
allowedPool = types.NewAllowedPool("Usdx", "uSdX")
err = allowedPool.Validate()
assert.Error(t, err)
}
func TestAllowedPool_String(t *testing.T) {
allowedPool := types.NewAllowedPool("hard", "ukava")
require.NoError(t, allowedPool.Validate())
output := `AllowedPool:
Name: hard/ukava
Token A: hard
Token B: ukava
`
assert.Equal(t, output, allowedPool.String())
}
func TestAllowedPool_Name(t *testing.T) {
testCases := []struct {
tokens string
name string
}{
{
tokens: "atoken btoken",
name: "atoken/btoken",
},
{
tokens: "aaa aaaa",
name: "aaa/aaaa",
},
{
tokens: "aaaa aaab",
name: "aaaa/aaab",
},
{
tokens: "a001 a002",
name: "a001/a002",
},
{
tokens: "hard ukava",
name: "hard/ukava",
},
{
tokens: "bnb hard",
name: "bnb/hard",
},
{
tokens: "bnb xrpb",
name: "bnb/xrpb",
},
}
for _, tc := range testCases {
t.Run(tc.tokens, func(t *testing.T) {
tokens := strings.Split(tc.tokens, " ")
require.Equal(t, 2, len(tokens))
allowedPool := types.NewAllowedPool(tokens[0], tokens[1])
require.NoError(t, allowedPool.Validate())
assert.Equal(t, tc.name, allowedPool.Name())
})
}
}
func TestAllowedPools_Validate(t *testing.T) {
testCases := []struct {
name string
allowedPools types.AllowedPools
expectedErr string
}{
{
name: "invalid pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("HARD", "UKAVA"),
),
expectedErr: "invalid denom: HARD",
},
{
name: "duplicate pool",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("hard", "ukava"),
),
expectedErr: "duplicate pool: hard/ukava",
},
{
name: "duplicate pools",
allowedPools: types.NewAllowedPools(
types.NewAllowedPool("hard", "ukava"),
types.NewAllowedPool("bnb", "usdx"),
types.NewAllowedPool("btcb", "xrpb"),
types.NewAllowedPool("bnb", "usdx"),
),
expectedErr: "duplicate pool: bnb/usdx",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.allowedPools.Validate()
assert.EqualError(t, err, tc.expectedErr)
})
}
}

6
x/swap/types/querier.go Normal file
View File

@ -0,0 +1,6 @@
package types
// Querier routes for the swap module
const (
QueryGetParams = "params"
)