mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05:18 +00:00
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:
parent
cae7503f7b
commit
9d9b169e6a
14
app/app.go
14
app/app.go
@ -43,6 +43,7 @@ import (
|
|||||||
"github.com/kava-labs/kava/x/issuance"
|
"github.com/kava-labs/kava/x/issuance"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
|
"github.com/kava-labs/kava/x/swap"
|
||||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -85,6 +86,7 @@ var (
|
|||||||
incentive.AppModuleBasic{},
|
incentive.AppModuleBasic{},
|
||||||
issuance.AppModuleBasic{},
|
issuance.AppModuleBasic{},
|
||||||
hard.AppModuleBasic{},
|
hard.AppModuleBasic{},
|
||||||
|
swap.AppModuleBasic{},
|
||||||
)
|
)
|
||||||
|
|
||||||
// module account permissions
|
// module account permissions
|
||||||
@ -158,6 +160,7 @@ type App struct {
|
|||||||
incentiveKeeper incentive.Keeper
|
incentiveKeeper incentive.Keeper
|
||||||
issuanceKeeper issuance.Keeper
|
issuanceKeeper issuance.Keeper
|
||||||
hardKeeper hard.Keeper
|
hardKeeper hard.Keeper
|
||||||
|
swapKeeper swap.Keeper
|
||||||
|
|
||||||
// the module manager
|
// the module manager
|
||||||
mm *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,
|
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
|
||||||
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
|
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
|
||||||
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
|
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
|
||||||
hard.StoreKey,
|
hard.StoreKey, swap.StoreKey,
|
||||||
)
|
)
|
||||||
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
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)
|
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
|
||||||
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
|
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
|
||||||
hardSubspace := app.paramsKeeper.Subspace(hard.DefaultParamspace)
|
hardSubspace := app.paramsKeeper.Subspace(hard.DefaultParamspace)
|
||||||
|
swapSubspace := app.paramsKeeper.Subspace(swap.DefaultParamspace)
|
||||||
|
|
||||||
// add keepers
|
// add keepers
|
||||||
app.accountKeeper = auth.NewAccountKeeper(
|
app.accountKeeper = auth.NewAccountKeeper(
|
||||||
@ -393,6 +397,11 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
|
|||||||
app.accountKeeper,
|
app.accountKeeper,
|
||||||
app.supplyKeeper,
|
app.supplyKeeper,
|
||||||
)
|
)
|
||||||
|
app.swapKeeper = swap.NewKeeper(
|
||||||
|
app.cdc,
|
||||||
|
keys[swap.StoreKey],
|
||||||
|
swapSubspace,
|
||||||
|
)
|
||||||
|
|
||||||
// register the staking hooks
|
// register the staking hooks
|
||||||
// NOTE: stakingKeeper above is passed by reference, so that it will contain these 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),
|
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||||
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper),
|
hard.NewAppModule(app.hardKeeper, app.supplyKeeper, app.pricefeedKeeper),
|
||||||
|
swap.NewAppModule(app.swapKeeper),
|
||||||
)
|
)
|
||||||
|
|
||||||
// During begin block slashing happens after distr.BeginBlocker so that
|
// 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,
|
validatorvesting.ModuleName, distr.ModuleName,
|
||||||
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
||||||
gov.ModuleName, mint.ModuleName, evidence.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,
|
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
|
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
|
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
|
||||||
|
@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/kava-labs/kava/x/issuance"
|
"github.com/kava-labs/kava/x/issuance"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
|
"github.com/kava-labs/kava/x/swap"
|
||||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
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) GetHardKeeper() hard.Keeper { return tApp.hardKeeper }
|
||||||
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
||||||
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
|
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
|
// 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 {
|
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
||||||
|
@ -2409,7 +2409,29 @@ paths:
|
|||||||
$ref: "#/definitions/Coin"
|
$ref: "#/definitions/Coin"
|
||||||
500:
|
500:
|
||||||
description: Server internal error
|
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:
|
/bank/accounts/{address}/transfers:
|
||||||
post:
|
post:
|
||||||
summary: Send coins from one account to another
|
summary: Send coins from one account to another
|
||||||
@ -4837,6 +4859,25 @@ definitions:
|
|||||||
active:
|
active:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: true
|
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:
|
HardParams:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
28
x/swap/alias.go
Normal file
28
x/swap/alias.go
Normal 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
|
||||||
|
)
|
58
x/swap/client/cli/query.go
Normal file
58
x/swap/client/cli/query.go
Normal 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, ¶ms); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal params: %w", err)
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(params)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
28
x/swap/client/cli/tx.go
Normal file
28
x/swap/client/cli/tx.go
Normal 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
|
||||||
|
}
|
37
x/swap/client/rest/query.go
Normal file
37
x/swap/client/rest/query.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
17
x/swap/client/rest/rest.go
Normal file
17
x/swap/client/rest/rest.go
Normal 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
9
x/swap/client/rest/tx.go
Normal 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
24
x/swap/genesis.go
Normal 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
17
x/swap/handler.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
x/swap/keeper/integration_test.go
Normal file
32
x/swap/keeper/integration_test.go
Normal 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
29
x/swap/keeper/keeper.go
Normal 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
19
x/swap/keeper/params.go
Normal 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, ¶ms)
|
||||||
|
}
|
39
x/swap/keeper/params_test.go
Normal file
39
x/swap/keeper/params_test.go
Normal 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
36
x/swap/keeper/querier.go
Normal 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
|
||||||
|
}
|
65
x/swap/keeper/querier_test.go
Normal file
65
x/swap/keeper/querier_test.go
Normal 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
166
x/swap/module.go
Normal 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
|
||||||
|
// }
|
13
x/swap/spec/01_concepts.md
Normal file
13
x/swap/spec/01_concepts.md
Normal 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
35
x/swap/spec/02_state.md
Normal 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"`
|
||||||
|
}
|
||||||
|
```
|
5
x/swap/spec/03_messages.md
Normal file
5
x/swap/spec/03_messages.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!--
|
||||||
|
order: 3
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Messages
|
9
x/swap/spec/04_events.md
Normal file
9
x/swap/spec/04_events.md
Normal 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
19
x/swap/spec/05_params.md
Normal 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
20
x/swap/spec/README.md
Normal 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
18
x/swap/types/codec.go
Normal 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
11
x/swap/types/errors.go
Normal 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
6
x/swap/types/events.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Event types for swap module
|
||||||
|
const (
|
||||||
|
// EventTypeCustom = ""
|
||||||
|
)
|
42
x/swap/types/genesis.go
Normal file
42
x/swap/types/genesis.go
Normal 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{})
|
||||||
|
}
|
168
x/swap/types/genesis_test.go
Normal file
168
x/swap/types/genesis_test.go
Normal 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
22
x/swap/types/keys.go
Normal 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
1
x/swap/types/msg.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package types
|
91
x/swap/types/params.go
Normal file
91
x/swap/types/params.go
Normal 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
233
x/swap/types/params_test.go
Normal 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), ¶ms)
|
||||||
|
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, ¶ms)
|
||||||
|
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(¶ms)
|
||||||
|
|
||||||
|
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
90
x/swap/types/pool.go
Normal 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
185
x/swap/types/pool_test.go
Normal 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
6
x/swap/types/querier.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Querier routes for the swap module
|
||||||
|
const (
|
||||||
|
QueryGetParams = "params"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user