diff --git a/Makefile b/Makefile index 8accba245..025c397e1 100644 --- a/Makefile +++ b/Makefile @@ -274,19 +274,19 @@ gateway_list: ## List all the staked gateways .PHONY: gateway_stake gateway_stake: ## Stake tokens for the gateway specified (must specify the gateway env var) - poktrolld --home=$(POKTROLLD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POKTROLLD_HOME) tx gateway stake-gateway --config $(POKTROLLD_HOME)/config/$(STAKE) --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_stake gateway1_stake: ## Stake gateway1 - GATEWAY=gateway1 make gateway_stake + GATEWAY=gateway1 STAKE=gateway1_stake_config.yaml make gateway_stake .PHONY: gateway2_stake gateway2_stake: ## Stake gateway2 - GATEWAY=gateway2 make gateway_stake + GATEWAY=gateway2 STAKE=gateway2_stake_config.yaml make gateway_stake .PHONY: gateway3_stake gateway3_stake: ## Stake gateway3 - GATEWAY=gateway3 make gateway_stake + GATEWAY=gateway3 STAKE=gateway3_stake_config.yaml make gateway_stake .PHONY: gateway_unstake gateway_unstake: ## Unstake an gateway (must specify the GATEWAY env var) diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index 7317156b0..c97017c2c 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -5,6 +5,7 @@ package e2e import ( "flag" "fmt" + "io/ioutil" "log" "os" "regexp" @@ -166,17 +167,36 @@ func (s *suite) TheUserShouldWaitForSeconds(dur int64) { } func (s *suite) TheUserStakesAWithUpoktFromTheAccount(actorType string, amount int64, accName string) { + // Create a temporary config file + configPathPattern := fmt.Sprintf("%s_stake_config_*.yaml", accName) + configContent := fmt.Sprintf(`stake_amount: %d upokt`, amount) + configFile, err := ioutil.TempFile("", configPathPattern) + if err != nil { + s.Fatalf("error creating config file: %q", err) + } + if _, err = configFile.Write([]byte(configContent)); err != nil { + s.Fatalf("error writing config file: %q", err) + } + args := []string{ "tx", actorType, fmt.Sprintf("stake-%s", actorType), - fmt.Sprintf("%dupokt", amount), + "--config", + configFile.Name(), "--from", accName, keyRingFlag, "-y", } res, err := s.pocketd.RunCommandOnHost("", args...) + + // Remove the temporary config file + err = os.Remove(configFile.Name()) + if err != nil { + s.Fatalf("error removing config file: %q", err) + } + if err != nil { s.Fatalf("error staking %s: %s", actorType, err) } diff --git a/localnet/poktrolld/config/gateway1_stake_config.yaml b/localnet/poktrolld/config/gateway1_stake_config.yaml new file mode 100644 index 000000000..2f085c5ef --- /dev/null +++ b/localnet/poktrolld/config/gateway1_stake_config.yaml @@ -0,0 +1 @@ +stake_amount: 1000upokt \ No newline at end of file diff --git a/localnet/poktrolld/config/gateway2_stake_config.yaml b/localnet/poktrolld/config/gateway2_stake_config.yaml new file mode 100644 index 000000000..2f085c5ef --- /dev/null +++ b/localnet/poktrolld/config/gateway2_stake_config.yaml @@ -0,0 +1 @@ +stake_amount: 1000upokt \ No newline at end of file diff --git a/localnet/poktrolld/config/gateway3_stake_config.yaml b/localnet/poktrolld/config/gateway3_stake_config.yaml new file mode 100644 index 000000000..2f085c5ef --- /dev/null +++ b/localnet/poktrolld/config/gateway3_stake_config.yaml @@ -0,0 +1 @@ +stake_amount: 1000upokt \ No newline at end of file diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index 3c4d29a30..3369f3885 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -1,41 +1,51 @@ package cli import ( + "os" "strconv" "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" + "github.com/pokt-network/poktroll/x/gateway/client/config" "github.com/pokt-network/poktroll/x/gateway/types" ) -var _ = strconv.Itoa(0) +var ( + flagStakeConfig string + _ = strconv.Itoa(0) +) func CmdStakeGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "stake-gateway ", + Use: "stake-gateway --config ", Short: "Stake a gateway", Long: `Stake a 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: -$ poktrolld --home=$(POKTROLLD_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) +$ poktrolld --home=$(POKTROLLD_HOME) tx gateway stake-gateway --config stake_config.yaml --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) (err error) { + configContent, err := os.ReadFile(flagStakeConfig) if err != nil { return err } - stakeString := args[0] - stake, err := sdk.ParseCoinNormalized(stakeString) + + gatewayStakeConfig, err := config.ParseGatewayConfig(configContent) if err != nil { return err } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + msg := types.NewMsgStakeGateway( clientCtx.GetFromAddress().String(), - stake, + gatewayStakeConfig.StakeAmount, ) if err := msg.ValidateBasic(); err != nil { return err @@ -44,6 +54,7 @@ $ poktrolld --home=$(POKTROLLD_HOME) tx gateway stake-gateway 1000upokt --keyrin }, } + cmd.Flags().StringVar(&flagStakeConfig, "config", "", "Path to the stake config file") flags.AddTxFlagsToCmd(cmd) return cmd diff --git a/x/gateway/client/cli/tx_stake_gateway_test.go b/x/gateway/client/cli/tx_stake_gateway_test.go index ce1fb39d0..f50549686 100644 --- a/x/gateway/client/cli/tx_stake_gateway_test.go +++ b/x/gateway/client/cli/tx_stake_gateway_test.go @@ -2,6 +2,7 @@ package cli_test import ( "fmt" + "os" "testing" sdkerrors "cosmossdk.io/errors" @@ -14,6 +15,7 @@ import ( "google.golang.org/grpc/status" "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/testutil/yaml" "github.com/pokt-network/poktroll/x/gateway/client/cli" "github.com/pokt-network/poktroll/x/gateway/types" ) @@ -39,57 +41,71 @@ func TestCLI_StakeGateway(t *testing.T) { } tests := []struct { - desc string - address string - stake string - err *sdkerrors.Error + desc string + address string + inputConfig string + expectedError *sdkerrors.Error }{ { desc: "stake gateway: invalid address", address: "invalid", - stake: "1000upokt", - err: types.ErrGatewayInvalidAddress, + inputConfig: ` + stake_amount: 1000upokt + `, + expectedError: types.ErrGatewayInvalidAddress, }, { desc: "stake gateway: missing address", // address: gatewayAccount.Address.String(), - stake: "1000upokt", - err: types.ErrGatewayInvalidAddress, + inputConfig: ` + stake_amount: 1000upokt + `, + expectedError: types.ErrGatewayInvalidAddress, }, { desc: "stake gateway: invalid stake amount (zero)", address: gatewayAccount.Address.String(), - stake: "0upokt", - err: types.ErrGatewayInvalidStake, + inputConfig: ` + stake_amount: 0upokt + `, + expectedError: types.ErrGatewayInvalidStake, }, { desc: "stake gateway: invalid stake amount (negative)", address: gatewayAccount.Address.String(), - stake: "-1000upokt", - err: types.ErrGatewayInvalidStake, + inputConfig: ` + stake_amount: -1000upokt + `, + expectedError: types.ErrGatewayInvalidStake, }, { desc: "stake gateway: invalid stake denom", address: gatewayAccount.Address.String(), - stake: "1000invalid", - err: types.ErrGatewayInvalidStake, + inputConfig: ` + stake_amount: 1000invalid + `, + expectedError: types.ErrGatewayInvalidStake, }, { desc: "stake gateway: invalid stake missing denom", address: gatewayAccount.Address.String(), - stake: "1000", - err: types.ErrGatewayInvalidStake, + inputConfig: ` + stake_amount: 1000 + `, + expectedError: types.ErrGatewayInvalidStake, }, { - desc: "stake gateway: invalid stake missing stake", - address: gatewayAccount.Address.String(), - // stake: "1000upokt", - err: types.ErrGatewayInvalidStake, + desc: "stake gateway: invalid stake missing stake", + address: gatewayAccount.Address.String(), + inputConfig: ``, + expectedError: types.ErrGatewayInvalidStake, }, { desc: "stake gateway: valid", address: gatewayAccount.Address.String(), - stake: "1000upokt", + inputConfig: ` + stake_amount: 1000upokt + `, }, } @@ -102,19 +118,23 @@ func TestCLI_StakeGateway(t *testing.T) { // Wait for a new block to be committed require.NoError(t, net.WaitForNextBlock()) + // write the stake config to a file + configPath := testutil.WriteToNewTempFile(t, yaml.NormalizeYAMLIndentation(tt.inputConfig)).Name() + t.Cleanup(func() { os.Remove(configPath) }) + // Prepare the arguments for the CLI command args := []string{ - tt.stake, + fmt.Sprintf("--config=%s", configPath), 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) + if tt.expectedError != nil { + stat, ok := status.FromError(tt.expectedError) require.True(t, ok) - require.Contains(t, stat.Message(), tt.err.Error()) + require.Contains(t, stat.Message(), tt.expectedError.Error()) return } require.NoError(t, err) diff --git a/x/gateway/client/config/errors.go b/x/gateway/client/config/errors.go new file mode 100644 index 000000000..aa01b3a46 --- /dev/null +++ b/x/gateway/client/config/errors.go @@ -0,0 +1,10 @@ +package config + +import sdkerrors "cosmossdk.io/errors" + +var ( + codespace = "gatewayconfig" + ErrGatewayConfigEmptyContent = sdkerrors.Register(codespace, 1, "empty gateway staking config content") + ErrGatewayConfigUnmarshalYAML = sdkerrors.Register(codespace, 2, "config reader cannot unmarshal yaml content") + ErrGatewayConfigInvalidStake = sdkerrors.Register(codespace, 3, "invalid stake in gateway stake config") +) diff --git a/x/gateway/client/config/gateway_config_reader.go b/x/gateway/client/config/gateway_config_reader.go new file mode 100644 index 000000000..6bf7aa3e3 --- /dev/null +++ b/x/gateway/client/config/gateway_config_reader.go @@ -0,0 +1,62 @@ +package config + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "gopkg.in/yaml.v2" +) + +// YAMLStakeGateway is the structure describing the gateway stake config file +type YAMLStakeGateway struct { + StakeAmount string `yaml:"stake_amount"` +} + +// GatewayStakeConfig is the structure describing the gateway stake config +type GatewayStakeConfig struct { + StakeAmount sdk.Coin +} + +// ParseGatewayConfig parses the gateway stake yaml config file into a StakeGatewayConfig struct +func ParseGatewayConfig(configContent []byte) (*GatewayStakeConfig, error) { + var stakeConfig *YAMLStakeGateway + + if len(configContent) == 0 { + return nil, ErrGatewayConfigEmptyContent + } + + // Unmarshal the stake config file into a stakeConfig + if err := yaml.Unmarshal(configContent, &stakeConfig); err != nil { + return nil, ErrGatewayConfigUnmarshalYAML.Wrap(err.Error()) + } + + // Validate the stake config + if len(stakeConfig.StakeAmount) == 0 { + return nil, ErrGatewayConfigInvalidStake + } + + // Parse the stake amount to a coin struct + stakeAmount, err := sdk.ParseCoinNormalized(stakeConfig.StakeAmount) + if err != nil { + return nil, ErrGatewayConfigInvalidStake.Wrap(err.Error()) + } + + // Basic validation of the stake amount + if err := stakeAmount.Validate(); err != nil { + return nil, ErrGatewayConfigInvalidStake.Wrap(err.Error()) + } + + if stakeAmount.IsZero() { + return nil, ErrGatewayConfigInvalidStake.Wrap("stake amount cannot be zero") + } + + // Only allow upokt coins staking + if stakeAmount.Denom != "upokt" { + return nil, ErrGatewayConfigInvalidStake.Wrapf( + "invalid stake denom, expecting: upokt, got: %s", + stakeAmount.Denom, + ) + } + + return &GatewayStakeConfig{ + StakeAmount: stakeAmount, + }, nil +} diff --git a/x/gateway/client/config/gateway_config_reader_test.go b/x/gateway/client/config/gateway_config_reader_test.go new file mode 100644 index 000000000..b9ec91f2e --- /dev/null +++ b/x/gateway/client/config/gateway_config_reader_test.go @@ -0,0 +1,83 @@ +package config_test + +import ( + "testing" + + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/gogo/status" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/testutil/yaml" + "github.com/pokt-network/poktroll/x/gateway/client/config" +) + +func Test_ParseGatewayStakeConfig(t *testing.T) { + tests := []struct { + desc string + expectedError *sdkerrors.Error + expectedConfig *config.GatewayStakeConfig + inputConfig string + }{ + // Valid Configs + { + desc: "valid gateway stake config", + inputConfig: ` + stake_amount: 1000upokt + `, + expectedError: nil, + expectedConfig: &config.GatewayStakeConfig{ + StakeAmount: sdk.NewCoin("upokt", sdk.NewInt(1000)), + }, + }, + // Invalid Configs + { + desc: "services_test: invalid service config with empty content", + expectedError: config.ErrGatewayConfigEmptyContent, + inputConfig: ``, + }, + { + desc: "invalid stake denom", + inputConfig: ` + stake_amount: 1000invalid + `, + expectedError: config.ErrGatewayConfigInvalidStake, + }, + { + desc: "negative stake amount", + inputConfig: ` + stake_amount: -1000upokt + `, + expectedError: config.ErrGatewayConfigInvalidStake, + }, + { + desc: "zero stake amount", + inputConfig: ` + stake_amount: 0upokt + `, + expectedError: config.ErrGatewayConfigInvalidStake, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + normalizedConfig := yaml.NormalizeYAMLIndentation(tt.inputConfig) + supplierServiceConfig, err := config.ParseGatewayConfig([]byte(normalizedConfig)) + + if tt.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectedError) + stat, ok := status.FromError(tt.expectedError) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.expectedError.Error()) + require.Nil(t, supplierServiceConfig) + return + } + + require.NoError(t, err) + + require.Equal(t, tt.expectedConfig.StakeAmount, supplierServiceConfig.StakeAmount) + require.Equal(t, tt.expectedConfig.StakeAmount.Denom, supplierServiceConfig.StakeAmount.Denom) + }) + } +}