From 6b5f30085ffb32a797cb0beb8ea643405841f0f9 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:01:10 +0100 Subject: [PATCH] [Gateway] Implement StakeGateway Message and Add Tests (#68) Co-authored-by: DK --- .github/workflows/go.yml | 3 - Makefile | 26 +++- cmd/pocketd/cmd/root.go | 2 + config.yml | 6 +- docs/static/openapi.yml | 82 +++++++++++ proto/pocket/gateway/gateway.proto | 6 +- proto/pocket/gateway/tx.proto | 9 +- testutil/gateway/mocks/mocks.go | 3 + testutil/keeper/gateway.go | 14 +- testutil/network/network.go | 34 +++-- x/application/client/cli/helpers_test.go | 6 +- x/gateway/client/cli/helpers_test.go | 21 +++ x/gateway/client/cli/query_gateway_test.go | 18 --- x/gateway/client/cli/tx_stake_gateway.go | 22 ++- x/gateway/client/cli/tx_stake_gateway_test.go | 130 ++++++++++++++++++ x/gateway/keeper/gateway_test.go | 17 ++- x/gateway/keeper/msg_server_stake_gateway.go | 74 +++++++++- .../keeper/msg_server_stake_gateway_test.go | 94 +++++++++++++ x/gateway/types/errors.go | 10 +- x/gateway/types/expected_keepers.go | 5 +- x/gateway/types/genesis.go | 24 +++- x/gateway/types/genesis_test.go | 122 +++++++++++++++- x/gateway/types/message_stake_gateway.go | 27 +++- x/gateway/types/message_stake_gateway_test.go | 50 ++++++- 24 files changed, 725 insertions(+), 80 deletions(-) create mode 100644 testutil/gateway/mocks/mocks.go create mode 100644 x/gateway/client/cli/helpers_test.go create mode 100644 x/gateway/client/cli/tx_stake_gateway_test.go create mode 100644 x/gateway/keeper/msg_server_stake_gateway_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 075f19a92..1ebbe4a07 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -38,8 +38,5 @@ jobs: - name: Build run: ignite chain build --debug --skip-proto - - name: Mockgen - run: make go_mockgen - - name: Test run: make go_test diff --git a/Makefile b/Makefile index 3e4ca8b4b..cd03769a6 100644 --- a/Makefile +++ b/Makefile @@ -180,6 +180,30 @@ todo_count: ## Print a count of all the TODOs in the project todo_this_commit: ## List all the TODOs needed to be done in this commit grep --exclude-dir={.git,vendor,prototype,.vscode} --exclude=Makefile -r -e "TODO_IN_THIS_COMMIT" -e "DISCUSS_IN_THIS_COMMIT" +#################### +### Gateways ### +#################### + +.PHONY: gateway_list +gateway_list: ## List all the staked gateways + pocketd --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) + +.PHONY: gateway_stake +gateway_stake: ## Stake tokens for the gateway specified (must specify the gateway env var) + pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(gateway) --node $(POCKET_NODE) + +.PHONY: gateway1_stake +gateway1_stake: ## Stake gateway1 + gateway=gateway1 make gateway_stake + +.PHONY: gateway2_stake +gateway2_stake: ## Stake gateway2 + gateway=gateway2 make gateway_stake + +.PHONY: gateway3_stake +gateway3_stake: ## Stake gateway3 + gateway=gateway3 make gateway_stake + #################### ### Applications ### #################### @@ -234,4 +258,4 @@ acc_balance_total_supply: ## Query the total supply of the network .PHONY: ignite_acc_list ignite_acc_list: ## List all the accounts in LocalNet - ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) \ No newline at end of file + ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) diff --git a/cmd/pocketd/cmd/root.go b/cmd/pocketd/cmd/root.go index fc84c4f81..59c45dcce 100644 --- a/cmd/pocketd/cmd/root.go +++ b/cmd/pocketd/cmd/root.go @@ -39,6 +39,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + // this line is used by starport scaffolding # root/moduleImport + "pocket/app" appparams "pocket/app/params" ) diff --git a/config.yml b/config.yml index 9e59134dc..4ad66fbc9 100644 --- a/config.yml +++ b/config.yml @@ -32,15 +32,15 @@ accounts: mnemonic: "client city senior tenant source soda spread buffalo shaft amused bar carbon keen off feel coral easily announce metal orphan sustain maple expand loop" coins: - 330000000upokt - - name: portal1 + - name: gateway1 mnemonic: "salt iron goat also absorb depend involve agent apology between lift shy door left bulb arrange industry father jelly olive rifle return predict into" coins: - 100000000upokt - - name: portal2 + - name: gateway2 mnemonic: "suffer wet jelly furnace cousin flip layer render finish frequent pledge feature economy wink like water disease final erase goat include apple state furnace" coins: - 200000000upokt - - name: portal3 + - name: gateway3 mnemonic: "elder spatial erosion soap athlete tide subject recipe also awkward head pattern cart version beach usual oxygen confirm erupt diamond maze smooth census garment" coins: - 300000000upokt diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index cfed8b38f..f51478c6d 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46690,6 +46690,23 @@ paths: properties: address: type: string + title: The Bech32 address of the gateway + stake: + title: The total amount of uPOKT the gateway has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. pagination: type: object properties: @@ -46810,6 +46827,23 @@ paths: properties: address: type: string + title: The Bech32 address of the gateway + stake: + title: The total amount of uPOKT the gateway has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. default: description: An unexpected error response. schema: @@ -76040,6 +76074,20 @@ definitions: properties: address: type: string + title: The Bech32 address of the gateway + stake: + title: The total amount of uPOKT the gateway has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. pocket.gateway.MsgStakeGatewayResponse: type: object pocket.gateway.MsgUnstakeGatewayResponse: @@ -76057,6 +76105,23 @@ definitions: properties: address: type: string + title: The Bech32 address of the gateway + stake: + title: The total amount of uPOKT the gateway has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. pagination: type: object properties: @@ -76091,6 +76156,23 @@ definitions: properties: address: type: string + title: The Bech32 address of the gateway + stake: + title: The total amount of uPOKT the gateway has staked + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. pocket.gateway.QueryParamsResponse: type: object properties: diff --git a/proto/pocket/gateway/gateway.proto b/proto/pocket/gateway/gateway.proto index 168f5e909..f2a450cb3 100644 --- a/proto/pocket/gateway/gateway.proto +++ b/proto/pocket/gateway/gateway.proto @@ -3,7 +3,11 @@ package pocket.gateway; option go_package = "pocket/x/gateway/types"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/base/v1beta1/coin.proto"; + message Gateway { - string address = 1; + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway + cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the gateway has staked } diff --git a/proto/pocket/gateway/tx.proto b/proto/pocket/gateway/tx.proto index b6ba4616f..0c568080c 100644 --- a/proto/pocket/gateway/tx.proto +++ b/proto/pocket/gateway/tx.proto @@ -4,13 +4,20 @@ package pocket.gateway; option go_package = "pocket/x/gateway/types"; +import "cosmos/msg/v1/msg.proto"; +import "cosmos_proto/cosmos.proto"; +import "cosmos/base/v1beta1/coin.proto"; + // Msg defines the Msg service. service Msg { rpc StakeGateway (MsgStakeGateway ) returns (MsgStakeGatewayResponse ); rpc UnstakeGateway (MsgUnstakeGateway) returns (MsgUnstakeGatewayResponse); } message MsgStakeGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + + string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway + cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the gateway is staking. Must be ≥ to the current amount that the gateway has staked (if any) } message MsgStakeGatewayResponse {} diff --git a/testutil/gateway/mocks/mocks.go b/testutil/gateway/mocks/mocks.go new file mode 100644 index 000000000..16355b5a1 --- /dev/null +++ b/testutil/gateway/mocks/mocks.go @@ -0,0 +1,3 @@ +package mocks + +// This file is in place to declare the package for dynamically generated mocks diff --git a/testutil/keeper/gateway.go b/testutil/keeper/gateway.go index 8447a7bf2..65f3da440 100644 --- a/testutil/keeper/gateway.go +++ b/testutil/keeper/gateway.go @@ -3,6 +3,11 @@ package keeper import ( "testing" + "pocket/x/gateway/keeper" + "pocket/x/gateway/types" + + mocks "pocket/testutil/gateway/mocks" + tmdb "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -12,9 +17,8 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" ) func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { @@ -30,6 +34,10 @@ func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { registry := codectypes.NewInterfaceRegistry() cdc := codec.NewProtoCodec(registry) + ctrl := gomock.NewController(t) + mockBankKeeper := mocks.NewMockBankKeeper(ctrl) + mockBankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes() + paramsSubspace := typesparams.NewSubspace(cdc, types.Amino, storeKey, @@ -41,7 +49,7 @@ func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey, memStoreKey, paramsSubspace, - nil, + mockBankKeeper, nil, ) diff --git a/testutil/network/network.go b/testutil/network/network.go index 8f9b66b73..1d8226ff7 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -2,6 +2,7 @@ package network import ( "fmt" + "strconv" "testing" "time" @@ -17,7 +18,6 @@ import ( clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/cosmos/cosmos-sdk/testutil/network" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" - cosmostypes "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" @@ -25,7 +25,8 @@ import ( "pocket/app" "pocket/testutil/nullify" "pocket/testutil/sample" - "pocket/x/application/types" + app_types "pocket/x/application/types" + gateway_types "pocket/x/gateway/types" ) type ( @@ -98,14 +99,14 @@ func DefaultConfig() network.Config { } } -// applicationModuleGenesis generates a GenesisState object with a given number of applications. +// DefaultApplicationModuleGenesisState generates a GenesisState object with a given number of applications. // It returns the populated GenesisState object. -func DefaultApplicationModuleGenesisState(t *testing.T, n int) *types.GenesisState { +func DefaultApplicationModuleGenesisState(t *testing.T, n int) *app_types.GenesisState { t.Helper() - state := types.DefaultGenesis() + state := app_types.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i+1))) - application := types.Application{ + application := app_types.Application{ Address: sample.AccAddress(), Stake: &stake, } @@ -115,16 +116,25 @@ func DefaultApplicationModuleGenesisState(t *testing.T, n int) *types.GenesisSta return state } -// HydrateGenesisState adds a given module's GenesisState to the network's genesis state. -func HydrateGenesisState(t *testing.T, cfg *network.Config, moduleGenesisState *types.GenesisState, moduleName string) { +// DefaultGatewayModuleGenesisState generates a GenesisState object with a given number of gateways. +// It returns the populated GenesisState object. +func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gateway_types.GenesisState { t.Helper() - buf, err := cfg.Codec.MarshalJSON(moduleGenesisState) - require.NoError(t, err) - cfg.GenesisState[moduleName] = buf + state := gateway_types.DefaultGenesis() + for i := 0; i < n; i++ { + stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) + gateway := gateway_types.Gateway{ + Address: strconv.Itoa(i), + Stake: &stake, + } + nullify.Fill(&gateway) + state.GatewayList = append(state.GatewayList, gateway) + } + return state } // Initialize an Account by sending it some funds from the validator in the network to the address provided -func InitAccount(t *testing.T, net *Network, addr cosmostypes.AccAddress) { +func InitAccount(t *testing.T, net *Network, addr sdk.AccAddress) { t.Helper() val := net.Validators[0] ctx := val.ClientCtx diff --git a/x/application/client/cli/helpers_test.go b/x/application/client/cli/helpers_test.go index 8d2d90bb4..aa60d57ed 100644 --- a/x/application/client/cli/helpers_test.go +++ b/x/application/client/cli/helpers_test.go @@ -8,6 +8,8 @@ import ( "pocket/cmd/pocketd/cmd" "pocket/testutil/network" "pocket/x/application/types" + + "github.com/stretchr/testify/require" ) // Dummy variable to avoid unused import error. @@ -24,6 +26,8 @@ func networkWithApplicationObjects(t *testing.T, n int) (*network.Network, []typ t.Helper() cfg := network.DefaultConfig() appGenesisState := network.DefaultApplicationModuleGenesisState(t, n) - network.HydrateGenesisState(t, &cfg, appGenesisState, types.ModuleName) + buf, err := cfg.Codec.MarshalJSON(appGenesisState) + require.NoError(t, err) + cfg.GenesisState[types.ModuleName] = buf return network.New(t, cfg), appGenesisState.ApplicationList } diff --git a/x/gateway/client/cli/helpers_test.go b/x/gateway/client/cli/helpers_test.go new file mode 100644 index 000000000..32f159e55 --- /dev/null +++ b/x/gateway/client/cli/helpers_test.go @@ -0,0 +1,21 @@ +package cli_test + +import ( + "testing" + + "pocket/testutil/network" + "pocket/x/gateway/types" + + "github.com/stretchr/testify/require" +) + +// networkWithGatewayObjects creates a network with a populated gateway state of n gateway objects +func networkWithGatewayObjects(t *testing.T, n int) (*network.Network, []types.Gateway) { + t.Helper() + cfg := network.DefaultConfig() + gatewayGenesisState := network.DefaultGatewayModuleGenesisState(t, n) + buf, err := cfg.Codec.MarshalJSON(gatewayGenesisState) + require.NoError(t, err) + cfg.GenesisState[types.ModuleName] = buf + return network.New(t, cfg), gatewayGenesisState.GatewayList +} diff --git a/x/gateway/client/cli/query_gateway_test.go b/x/gateway/client/cli/query_gateway_test.go index db9a9691c..37e0ec3b3 100644 --- a/x/gateway/client/cli/query_gateway_test.go +++ b/x/gateway/client/cli/query_gateway_test.go @@ -12,7 +12,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/network" "pocket/testutil/nullify" "pocket/x/gateway/client/cli" "pocket/x/gateway/types" @@ -21,23 +20,6 @@ import ( // Prevent strconv unused error var _ = strconv.IntSize -func networkWithGatewayObjects(t *testing.T, n int) (*network.Network, []types.Gateway) { - t.Helper() - cfg := network.DefaultConfig() - state := types.GenesisState{} - for i := 0; i < n; i++ { - gateway := types.Gateway{ - Address: strconv.Itoa(i), - } - nullify.Fill(&gateway) - state.GatewayList = append(state.GatewayList, gateway) - } - buf, err := cfg.Codec.MarshalJSON(&state) - require.NoError(t, err) - cfg.GenesisState[types.ModuleName] = buf - return network.New(t, cfg), state.GatewayList -} - func TestShowGateway(t *testing.T) { net, objs := networkWithGatewayObjects(t, 2) diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index 8a21c5129..29ee3873b 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -3,29 +3,39 @@ package cli import ( "strconv" + "pocket/x/gateway/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" - "pocket/x/gateway/types" ) var _ = strconv.Itoa(0) func CmdStakeGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "stake-gateway", - Short: "Broadcast message stake-gateway", - Args: cobra.ExactArgs(0), + Use: "stake-gateway [amount]", + Short: "Stake an gateway", + Long: `Stake an gateway with the provided parameters. This is a broadcast operation that +will stake the tokens and associate them with the gateway specified by the 'from' address. +Example: +$ pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err } - + stakeString := args[0] + stake, err := sdk.ParseCoinNormalized(stakeString) + if err != nil { + return err + } msg := types.NewMsgStakeGateway( clientCtx.GetFromAddress().String(), + stake, ) if err := msg.ValidateBasic(); err != nil { return err diff --git a/x/gateway/client/cli/tx_stake_gateway_test.go b/x/gateway/client/cli/tx_stake_gateway_test.go new file mode 100644 index 000000000..110e7f661 --- /dev/null +++ b/x/gateway/client/cli/tx_stake_gateway_test.go @@ -0,0 +1,130 @@ +package cli_test + +import ( + "fmt" + "testing" + + "pocket/testutil/network" + "pocket/x/gateway/client/cli" + "pocket/x/gateway/types" + + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" +) + +func TestCLI_StakeGateway(t *testing.T) { + net, _ := networkWithGatewayObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the gateway to be staked + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 1) + gatewayAccount := accounts[0] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + address string + stakeAmount string + err *errors.Error + }{ + { + desc: "stake gateway: invalid address", + address: "invalid", + stakeAmount: "1000upokt", + err: types.ErrGatewayInvalidAddress, + }, + { + desc: "stake gateway: missing address", + // address: gatewayAccount.Address.String(), + stakeAmount: "1000upokt", + err: types.ErrGatewayInvalidAddress, + }, + { + desc: "stake gateway: invalid stake amount (zero)", + address: gatewayAccount.Address.String(), + stakeAmount: "0upokt", + err: types.ErrGatewayInvalidStake, + }, + { + desc: "stake gateway: invalid stake amount (negative)", + address: gatewayAccount.Address.String(), + stakeAmount: "-1000upokt", + err: types.ErrGatewayInvalidStake, + }, + { + desc: "stake gateway: invalid stake denom", + address: gatewayAccount.Address.String(), + stakeAmount: "1000invalid", + err: types.ErrGatewayInvalidStake, + }, + { + desc: "stake gateway: invalid stake missing denom", + address: gatewayAccount.Address.String(), + stakeAmount: "1000", + err: types.ErrGatewayInvalidStake, + }, + { + desc: "stake gateway: invalid stake missing stake", + address: gatewayAccount.Address.String(), + // stakeAmount: "1000upokt", + err: types.ErrGatewayInvalidStake, + }, + { + desc: "stake gateway: valid", + address: gatewayAccount.Address.String(), + stakeAmount: "1000upokt", + }, + } + + // Initialize the Gateway Account by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, gatewayAccount.Address) + + // Stake the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.stakeAmount, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.address), + } + args = append(args, commonArgs...) + + // Execute the command + outStake, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdStakeGateway(), args) + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NoError(t, err) + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(outStake.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/gateway/keeper/gateway_test.go b/x/gateway/keeper/gateway_test.go index ff8c43a28..b61928b70 100644 --- a/x/gateway/keeper/gateway_test.go +++ b/x/gateway/keeper/gateway_test.go @@ -4,17 +4,24 @@ import ( "strconv" "testing" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" + "pocket/cmd/pocketd/cmd" keepertest "pocket/testutil/keeper" "pocket/testutil/nullify" "pocket/x/gateway/keeper" "pocket/x/gateway/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" ) // Prevent strconv unused error var _ = strconv.IntSize +func init() { + cmd.InitSDKConfig() +} + func createNGateway(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Gateway { items := make([]types.Gateway, n) for i := range items { @@ -25,6 +32,11 @@ func createNGateway(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Gatew return items } +func TestGatewayModuleAddress(t *testing.T) { + moduleAddress := authtypes.NewModuleAddress(types.ModuleName) + require.Equal(t, "pokt1f6j7u6875p2cvyrgjr0d2uecyzah0kget9vlpl", moduleAddress.String()) +} + func TestGatewayGet(t *testing.T) { keeper, ctx := keepertest.GatewayKeeper(t) items := createNGateway(keeper, ctx, 10) @@ -39,6 +51,7 @@ func TestGatewayGet(t *testing.T) { ) } } + func TestGatewayRemove(t *testing.T) { keeper, ctx := keepertest.GatewayKeeper(t) items := createNGateway(keeper, ctx, 10) diff --git a/x/gateway/keeper/msg_server_stake_gateway.go b/x/gateway/keeper/msg_server_stake_gateway.go index 33682cba1..fc5e6f639 100644 --- a/x/gateway/keeper/msg_server_stake_gateway.go +++ b/x/gateway/keeper/msg_server_stake_gateway.go @@ -3,15 +3,81 @@ package keeper import ( "context" - sdk "github.com/cosmos/cosmos-sdk/types" "pocket/x/gateway/types" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" ) -func (k msgServer) StakeGateway(goCtx context.Context, msg *types.MsgStakeGateway) (*types.MsgStakeGatewayResponse, error) { +func (k msgServer) StakeGateway( + goCtx context.Context, + msg *types.MsgStakeGateway, +) (*types.MsgStakeGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // TODO: Handling the message - _ = ctx + logger := k.Logger(ctx).With("method", "StakeGateway") + logger.Info("About to stake gateway with msg: %v", msg) + + // Check if the gateway already exists or not + var err error + var coinsToDelegate sdk.Coin + gateway, isGatewayFound := k.GetGateway(ctx, msg.Address) + if !isGatewayFound { + logger.Info("Gateway not found. Creating new gateway for address %s", msg.Address) + gateway = k.createGateway(ctx, msg) + coinsToDelegate = *msg.Stake + } else { + logger.Info("Gateway found. Updating gateway stake for address %s", msg.Address) + currGatewayStake := *gateway.Stake + if err = k.updateGateway(ctx, &gateway, msg); err != nil { + return nil, err + } + coinsToDelegate = (*msg.Stake).Sub(currGatewayStake) + } + + // Retrieve the address of the gateway + gatewayAddress, err := sdk.AccAddressFromBech32(msg.Address) + if err != nil { + logger.Error("could not parse address %s", msg.Address) + return nil, err + } + + // Send the coins from the gateway to the staked gateway pool + err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, gatewayAddress, types.ModuleName, []sdk.Coin{coinsToDelegate}) + if err != nil { + logger.Error("could not send %v coins from %s to %s module account due to %v", coinsToDelegate, gatewayAddress, types.ModuleName, err) + return nil, err + } + + // Update the Gateway in the store + k.SetGateway(ctx, gateway) + logger.Info("Successfully updated stake for gateway: %+v", gateway) return &types.MsgStakeGatewayResponse{}, nil } + +func (k msgServer) createGateway(ctx sdk.Context, msg *types.MsgStakeGateway) types.Gateway { + return types.Gateway{ + Address: msg.Address, + Stake: msg.Stake, + } +} + +func (k msgServer) updateGateway( + ctx sdk.Context, + gateway *types.Gateway, + msg *types.MsgStakeGateway, +) error { + // Checks if the the msg address is the same as the current owner + if msg.Address != gateway.Address { + return errors.Wrapf(types.ErrGatewayUnauthorized, "msg Address (%s) != gateway address (%s)", msg.Address, gateway.Address) + } + if msg.Stake == nil { + return errors.Wrapf(types.ErrGatewayInvalidStake, "stake amount cannot be nil") + } + if msg.Stake.IsLTE(*gateway.Stake) { + return errors.Wrapf(types.ErrGatewayInvalidStake, "stake amount %v must be higher than previous stake amount %v", msg.Stake, gateway.Stake) + } + gateway.Stake = msg.Stake + return nil +} diff --git a/x/gateway/keeper/msg_server_stake_gateway_test.go b/x/gateway/keeper/msg_server_stake_gateway_test.go new file mode 100644 index 000000000..dfcb98e4a --- /dev/null +++ b/x/gateway/keeper/msg_server_stake_gateway_test.go @@ -0,0 +1,94 @@ +package keeper_test + +import ( + "testing" + + keepertest "pocket/testutil/keeper" + + "pocket/testutil/sample" + "pocket/x/gateway/keeper" + "pocket/x/gateway/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestMsgServer_StakeGateway_SuccessfulCreateAndUpdate(t *testing.T) { + k, ctx := keepertest.GatewayKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the gateway + addr := sample.AccAddress() + + // Verify that the gateway does not exist yet + _, isGatewayFound := k.GetGateway(ctx, addr) + require.False(t, isGatewayFound) + + // Prepare the gateway + initialStake := sdk.NewCoin("upokt", sdk.NewInt(100)) + gateway := &types.MsgStakeGateway{ + Address: addr, + Stake: &initialStake, + } + + // Stake the gateway + _, err := srv.StakeGateway(wctx, gateway) + require.NoError(t, err) + + // Verify that the gateway exists + foundGateway, isGatewayFound := k.GetGateway(ctx, addr) + require.True(t, isGatewayFound) + require.Equal(t, addr, foundGateway.Address) + require.Equal(t, initialStake.Amount, foundGateway.Stake.Amount) + + // Prepare an updated gateway with a higher stake + updatedStake := sdk.NewCoin("upokt", sdk.NewInt(200)) + updatedGateway := &types.MsgStakeGateway{ + Address: addr, + Stake: &updatedStake, + } + + // Update the staked gateway + _, err = srv.StakeGateway(wctx, updatedGateway) + require.NoError(t, err) + foundGateway, isGatewayFound = k.GetGateway(ctx, addr) + require.True(t, isGatewayFound) + require.Equal(t, updatedStake.Amount, foundGateway.Stake.Amount) +} + +func TestMsgServer_StakeGateway_FailLoweringStake(t *testing.T) { + k, ctx := keepertest.GatewayKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Prepare the gateway + addr := sample.AccAddress() + initialStake := sdk.NewCoin("upokt", sdk.NewInt(100)) + gateway := &types.MsgStakeGateway{ + Address: addr, + Stake: &initialStake, + } + + // Stake the gateway & verify that the gateway exists + _, err := srv.StakeGateway(wctx, gateway) + require.NoError(t, err) + _, isGatewayFound := k.GetGateway(ctx, addr) + require.True(t, isGatewayFound) + + // Prepare an updated gateway with a lower stake + updatedStake := sdk.NewCoin("upokt", sdk.NewInt(50)) + updatedGateway := &types.MsgStakeGateway{ + Address: addr, + Stake: &updatedStake, + } + + // Verify that it fails + _, err = srv.StakeGateway(wctx, updatedGateway) + require.Error(t, err) + + // Verify that the gateway stake is unchanged + gatewayFound, isGatewayFound := k.GetGateway(ctx, addr) + require.True(t, isGatewayFound) + require.Equal(t, initialStake.Amount, gatewayFound.Stake.Amount) +} diff --git a/x/gateway/types/errors.go b/x/gateway/types/errors.go index 32dfef208..a52003b29 100644 --- a/x/gateway/types/errors.go +++ b/x/gateway/types/errors.go @@ -1,12 +1,12 @@ package types -// DONTCOVER +import "cosmossdk.io/errors" -import ( - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" -) +// DONTCOVER // x/gateway module sentinel errors var ( - ErrSample = sdkerrors.Register(ModuleName, 1100, "sample error") + ErrGatewayInvalidAddress = errors.Register(ModuleName, 1, "invalid gateway address") + ErrGatewayInvalidStake = errors.Register(ModuleName, 2, "invalid gateway stake") + ErrGatewayUnauthorized = errors.Register(ModuleName, 3, "unauthorized signer") ) diff --git a/x/gateway/types/expected_keepers.go b/x/gateway/types/expected_keepers.go index 6aa6e9778..257c7db5c 100644 --- a/x/gateway/types/expected_keepers.go +++ b/x/gateway/types/expected_keepers.go @@ -1,5 +1,7 @@ package types +//go:generate mockgen -destination ../../../testutil/gateway/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper + import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -13,6 +15,5 @@ type AccountKeeper interface { // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { - SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins - // Methods imported from bank should be defined here + DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error } diff --git a/x/gateway/types/genesis.go b/x/gateway/types/genesis.go index e06a6c5cb..e4595b667 100644 --- a/x/gateway/types/genesis.go +++ b/x/gateway/types/genesis.go @@ -1,7 +1,8 @@ package types import ( - "fmt" + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" ) // DefaultIndex is the default global index @@ -19,15 +20,32 @@ func DefaultGenesis() *GenesisState { // Validate performs basic genesis state validation returning an error upon any // failure. func (gs GenesisState) Validate() error { - // Check for duplicated index in gateway gatewayIndexMap := make(map[string]struct{}) for _, elem := range gs.GatewayList { + // Check for duplicated index in gateway index := string(GatewayKey(elem.Address)) if _, ok := gatewayIndexMap[index]; ok { - return fmt.Errorf("duplicated index for gateway") + return errors.Wrap(ErrGatewayInvalidAddress, "duplicated index for gateway") } gatewayIndexMap[index] = struct{}{} + // Validate the stake of each gateway + if elem.Stake == nil { + return errors.Wrap(ErrGatewayInvalidStake, "nil stake amount for gateway") + } + stakeAmount, err := sdk.ParseCoinNormalized(elem.Stake.String()) + if !stakeAmount.IsValid() { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid stake amount for gateway %v; (%v)", elem.Stake, stakeAmount.Validate()) + } + if err != nil { + return errors.Wrapf(ErrGatewayInvalidStake, "cannot parse stake amount for gateway %v; (%v)", elem.Stake, err) + } + if stakeAmount.IsZero() || stakeAmount.IsNegative() { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid stake amount for gateway: %v <= 0", elem.Stake) + } + if stakeAmount.Denom != "upokt" { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid stake amount denom for gateway %v", elem.Stake) + } } // this line is used by starport scaffolding # genesis/types/validate diff --git a/x/gateway/types/genesis_test.go b/x/gateway/types/genesis_test.go index 3227a7cb0..e08ce6f13 100644 --- a/x/gateway/types/genesis_test.go +++ b/x/gateway/types/genesis_test.go @@ -3,11 +3,20 @@ package types_test import ( "testing" - "github.com/stretchr/testify/require" + "pocket/testutil/sample" "pocket/x/gateway/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" ) func TestGenesisState_Validate(t *testing.T) { + addr1 := sample.AccAddress() + stake1 := sdk.NewCoin("upokt", sdk.NewInt(100)) + + addr2 := sample.AccAddress() + stake2 := sdk.NewCoin("upokt", sdk.NewInt(100)) + tests := []struct { desc string genState *types.GenesisState @@ -21,13 +30,14 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ - GatewayList: []types.Gateway{ { - Address: "0", + Address: addr1, + Stake: &stake1, }, { - Address: "1", + Address: addr2, + Stake: &stake2, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -35,14 +45,112 @@ func TestGenesisState_Validate(t *testing.T) { valid: true, }, { - desc: "duplicated gateway", + desc: "invalid - duplicated gateway address", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr1, + Stake: &stake2, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - nil gateway stake", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr2, + Stake: nil, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - missing gateway stake", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr2, + // Stake: stake2, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - zero gateway stake", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - negative gateway stake", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - wrong stake denom", + genState: &types.GenesisState{ + GatewayList: []types.Gateway{ + { + Address: addr1, + Stake: &stake1, + }, + { + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - missing denom", genState: &types.GenesisState{ GatewayList: []types.Gateway{ { - Address: "0", + Address: addr1, + Stake: &stake1, }, { - Address: "0", + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, }, }, }, diff --git a/x/gateway/types/message_stake_gateway.go b/x/gateway/types/message_stake_gateway.go index bb64360f9..4e0ef5d3c 100644 --- a/x/gateway/types/message_stake_gateway.go +++ b/x/gateway/types/message_stake_gateway.go @@ -1,17 +1,19 @@ package types import ( + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + types "github.com/cosmos/cosmos-sdk/types" ) const TypeMsgStakeGateway = "stake_gateway" var _ sdk.Msg = &MsgStakeGateway{} -func NewMsgStakeGateway(address string) *MsgStakeGateway { +func NewMsgStakeGateway(address string, stake types.Coin) *MsgStakeGateway { return &MsgStakeGateway{ Address: address, + Stake: &stake, } } @@ -37,9 +39,28 @@ func (msg *MsgStakeGateway) GetSignBytes() []byte { } func (msg *MsgStakeGateway) ValidateBasic() error { + // Validate the address _, err := sdk.AccAddressFromBech32(msg.Address) if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + return errors.Wrapf(ErrGatewayInvalidAddress, "invalid gateway address %s; (%v)", msg.Address, err) + } + + // Validate the stake amount + if msg.Stake == nil { + return errors.Wrapf(ErrGatewayInvalidStake, "nil gateway stake; (%v)", err) + } + stakeAmount, err := sdk.ParseCoinNormalized(msg.Stake.String()) + if !stakeAmount.IsValid() { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid gateway stake %v; (%v)", msg.Stake, stakeAmount.Validate()) + } + if err != nil { + return errors.Wrapf(ErrGatewayInvalidStake, "cannot parse gateway stake %v; (%v)", msg.Stake, err) + } + if stakeAmount.IsZero() || stakeAmount.IsNegative() { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid stake amount for gateway: %v <= 0", msg.Stake) + } + if stakeAmount.Denom != "upokt" { + return errors.Wrapf(ErrGatewayInvalidStake, "invalid stake amount denom for gateway %v", msg.Stake) } return nil } diff --git a/x/gateway/types/message_stake_gateway_test.go b/x/gateway/types/message_stake_gateway_test.go index a3852f041..ae3659ade 100644 --- a/x/gateway/types/message_stake_gateway_test.go +++ b/x/gateway/types/message_stake_gateway_test.go @@ -3,30 +3,70 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/stretchr/testify/require" "pocket/testutil/sample" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" ) func TestMsgStakeGateway_ValidateBasic(t *testing.T) { + coins := sdk.NewCoin("upokt", sdk.NewInt(100)) tests := []struct { name string msg MsgStakeGateway err error }{ { - name: "invalid address", + name: "invalid address - no stake", msg: MsgStakeGateway{ Address: "invalid_address", + // Stake explicitly nil + }, + err: ErrGatewayInvalidAddress, + }, { + name: "valid address - nil stake", + msg: MsgStakeGateway{ + Address: sample.AccAddress(), + // Stake explicitly nil + }, + err: ErrGatewayInvalidStake, + }, { + name: "valid address - zero stake", + msg: MsgStakeGateway{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + }, + err: ErrGatewayInvalidStake, + }, { + name: "valid address - negative stake", + msg: MsgStakeGateway{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + }, + err: ErrGatewayInvalidStake, + }, { + name: "valid address - invalid stake denom", + msg: MsgStakeGateway{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + }, + err: ErrGatewayInvalidStake, + }, { + name: "valid address - invalid stake missing denom", + msg: MsgStakeGateway{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, }, - err: sdkerrors.ErrInvalidAddress, + err: ErrGatewayInvalidStake, }, { - name: "valid address", + name: "valid address - valid stake", msg: MsgStakeGateway{ Address: sample.AccAddress(), + Stake: &coins, }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic()