diff --git a/docs/docs/guide/03-blog/02-connect-blockchain.md b/docs/docs/guide/03-blog/02-connect-blockchain.md index a11eac3ad2..db529a52a4 100644 --- a/docs/docs/guide/03-blog/02-connect-blockchain.md +++ b/docs/docs/guide/03-blog/02-connect-blockchain.md @@ -116,7 +116,7 @@ func main() { // Broadcast a transaction from account `alice` with the message // to create a post store response in txResp - txResp, err := cosmos.BroadcastTx(accountName, msg) + txResp, err := cosmos.BroadcastTx(account, msg) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index e297b6159e..8f664f1a14 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( golang.org/x/text v0.3.7 google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -235,7 +236,6 @@ require ( google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.6 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/ignite/cmd/cmd.go b/ignite/cmd/cmd.go index e0bcd8fda4..e285f2363a 100644 --- a/ignite/cmd/cmd.go +++ b/ignite/cmd/cmd.go @@ -69,6 +69,7 @@ ignite scaffold chain github.com/username/mars`, c.AddCommand(NewChain()) c.AddCommand(NewGenerate()) c.AddCommand(NewNetwork()) + c.AddCommand(NewNode()) c.AddCommand(NewAccount()) c.AddCommand(NewRelayer()) c.AddCommand(NewTools()) diff --git a/ignite/cmd/network.go b/ignite/cmd/network.go index f76861a8f6..af7c30b26a 100644 --- a/ignite/cmd/network.go +++ b/ignite/cmd/network.go @@ -156,6 +156,7 @@ func getNetworkCosmosClient(cmd *cobra.Command) (cosmosclient.Client, error) { cosmosclient.WithAddressPrefix(networktypes.SPN), cosmosclient.WithUseFaucet(spnFaucetAddress, networktypes.SPNDenom, 5), cosmosclient.WithKeyringServiceName(cosmosaccount.KeyringServiceName), + cosmosclient.WithKeyringDir(getKeyringDir(cmd)), } keyringBackend := getKeyringBackend(cmd) diff --git a/ignite/cmd/network_campaign_publish.go b/ignite/cmd/network_campaign_publish.go index 594ca2d191..1faa789ca9 100644 --- a/ignite/cmd/network_campaign_publish.go +++ b/ignite/cmd/network_campaign_publish.go @@ -23,6 +23,7 @@ func NewNetworkCampaignPublish() *cobra.Command { c.Flags().String(flagMetadata, "", "Add a metada to the chain") c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagSetHome()) return c } diff --git a/ignite/cmd/network_campaign_update.go b/ignite/cmd/network_campaign_update.go index 7734bc03df..6bd1916cba 100644 --- a/ignite/cmd/network_campaign_update.go +++ b/ignite/cmd/network_campaign_update.go @@ -30,6 +30,7 @@ func NewNetworkCampaignUpdate() *cobra.Command { c.Flags().String(flagCampaignTotalSupply, "", "Update the total of the mainnet of a campaign") c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_chain_init.go b/ignite/cmd/network_chain_init.go index e35903349c..ed2d155a42 100644 --- a/ignite/cmd/network_chain_init.go +++ b/ignite/cmd/network_chain_init.go @@ -47,6 +47,7 @@ func NewNetworkChainInit() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetCheckDependencies()) return c diff --git a/ignite/cmd/network_chain_join.go b/ignite/cmd/network_chain_join.go index a8475685df..5099a4d20d 100644 --- a/ignite/cmd/network_chain_join.go +++ b/ignite/cmd/network_chain_join.go @@ -38,6 +38,7 @@ func NewNetworkChainJoin() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetCheckDependencies()) diff --git a/ignite/cmd/network_chain_launch.go b/ignite/cmd/network_chain_launch.go index 566c058f42..7406bd36b6 100644 --- a/ignite/cmd/network_chain_launch.go +++ b/ignite/cmd/network_chain_launch.go @@ -24,6 +24,7 @@ func NewNetworkChainLaunch() *cobra.Command { c.Flags().Duration(flagRemainingTime, 0, "Duration of time in seconds before the chain is effectively launched") c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_chain_prepare.go b/ignite/cmd/network_chain_prepare.go index 4db17af415..8ac7102239 100644 --- a/ignite/cmd/network_chain_prepare.go +++ b/ignite/cmd/network_chain_prepare.go @@ -33,6 +33,7 @@ func NewNetworkChainPrepare() *cobra.Command { c.Flags().BoolP(flagForce, "f", false, "Force the prepare command to run even if the chain is not launched") c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetCheckDependencies()) diff --git a/ignite/cmd/network_chain_publish.go b/ignite/cmd/network_chain_publish.go index e280d51f91..78c0fc01ae 100644 --- a/ignite/cmd/network_chain_publish.go +++ b/ignite/cmd/network_chain_publish.go @@ -56,6 +56,7 @@ func NewNetworkChainPublish() *cobra.Command { c.Flags().String(flagAmount, "", "Amount of coins for account request") c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetCheckDependencies()) diff --git a/ignite/cmd/network_chain_revert_launch.go b/ignite/cmd/network_chain_revert_launch.go index cf83be77d7..d5732e1f1c 100644 --- a/ignite/cmd/network_chain_revert_launch.go +++ b/ignite/cmd/network_chain_revert_launch.go @@ -20,6 +20,7 @@ func NewNetworkChainRevertLaunch() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_request_approve.go b/ignite/cmd/network_request_approve.go index 12baae54d9..1592c5773c 100644 --- a/ignite/cmd/network_request_approve.go +++ b/ignite/cmd/network_request_approve.go @@ -30,6 +30,7 @@ func NewNetworkRequestApprove() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_request_reject.go b/ignite/cmd/network_request_reject.go index 1a470539d5..34c7baaa57 100644 --- a/ignite/cmd/network_request_reject.go +++ b/ignite/cmd/network_request_reject.go @@ -22,6 +22,7 @@ func NewNetworkRequestReject() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_request_verify.go b/ignite/cmd/network_request_verify.go index 0e22746541..fa0ca7b1ec 100644 --- a/ignite/cmd/network_request_verify.go +++ b/ignite/cmd/network_request_verify.go @@ -28,6 +28,7 @@ func NewNetworkRequestVerify() *cobra.Command { c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } diff --git a/ignite/cmd/network_reward_set.go b/ignite/cmd/network_reward_set.go index c2bfb21122..c64ba13e4e 100644 --- a/ignite/cmd/network_reward_set.go +++ b/ignite/cmd/network_reward_set.go @@ -21,6 +21,7 @@ func NewNetworkRewardSet() *cobra.Command { RunE: networkChainRewardSetHandler, } c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetHome()) return c diff --git a/ignite/cmd/node.go b/ignite/cmd/node.go new file mode 100644 index 0000000000..5a91fc9835 --- /dev/null +++ b/ignite/cmd/node.go @@ -0,0 +1,67 @@ +package ignitecmd + +import ( + "github.com/ignite/cli/ignite/pkg/cosmosclient" + "github.com/ignite/cli/ignite/pkg/xurl" + "github.com/spf13/cobra" +) + +const ( + flagNode = "node" + cosmosRPCAddress = "https://rpc.cosmos.network:443" +) + +func NewNode() *cobra.Command { + c := &cobra.Command{ + Use: "node [command]", + Short: "Make calls to a live blockchain node", + Args: cobra.ExactArgs(1), + } + + c.PersistentFlags().String(flagNode, cosmosRPCAddress, ": to tendermint rpc interface for this chain") + + c.AddCommand(NewNodeQuery()) + c.AddCommand(NewNodeTx()) + + return c +} + +func newNodeCosmosClient(cmd *cobra.Command) (cosmosclient.Client, error) { + var ( + home = getHome(cmd) + prefix = getAddressPrefix(cmd) + node = getRPC(cmd) + keyringBackend = getKeyringBackend(cmd) + keyringDir = getKeyringDir(cmd) + gas = getGas(cmd) + gasPrices = getGasPrices(cmd) + fees = getFees(cmd) + broadcastMode = getBroadcastMode(cmd) + ) + + options := []cosmosclient.Option{ + cosmosclient.WithAddressPrefix(prefix), + cosmosclient.WithHome(home), + cosmosclient.WithKeyringBackend(keyringBackend), + cosmosclient.WithKeyringDir(keyringDir), + cosmosclient.WithNodeAddress(xurl.HTTPEnsurePort(node)), + cosmosclient.WithBroadcastMode(broadcastMode), + } + + if gas != "" { + options = append(options, cosmosclient.WithGas(gas)) + } + if gasPrices != "" { + options = append(options, cosmosclient.WithGasPrices(gasPrices)) + } + if fees != "" { + options = append(options, cosmosclient.WithFees(fees)) + } + + return cosmosclient.New(cmd.Context(), options...) +} + +func getRPC(cmd *cobra.Command) (rpc string) { + rpc, _ = cmd.Flags().GetString(flagNode) + return +} diff --git a/ignite/cmd/node_query.go b/ignite/cmd/node_query.go new file mode 100644 index 0000000000..0acdd4499f --- /dev/null +++ b/ignite/cmd/node_query.go @@ -0,0 +1,72 @@ +package ignitecmd + +import ( + "errors" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" +) + +const ( + flagPage = "page" + flagLimit = "limit" + flagPageKey = "page-key" + flagOffset = "offset" + flagCountTotal = "count-total" + flagReverse = "reverse" +) + +func NewNodeQuery() *cobra.Command { + c := &cobra.Command{ + Use: "query", + Short: "Querying subcommands", + Aliases: []string{"q"}, + } + + c.AddCommand(NewNodeQueryBank()) + c.AddCommand(NewNodeQueryTx()) + + return c +} + +func flagSetPagination(query string) *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + + fs.Uint64(flagPage, 1, fmt.Sprintf("pagination page of %s to query for. This sets offset to a multiple of limit", query)) + fs.String(flagPageKey, "", fmt.Sprintf("pagination page-key of %s to query for", query)) + fs.Uint64(flagOffset, 0, fmt.Sprintf("pagination offset of %s to query for", query)) + fs.Uint64(flagLimit, 100, fmt.Sprintf("pagination limit of %s to query for", query)) + fs.Bool(flagCountTotal, false, fmt.Sprintf("count total number of records in %s to query for", query)) + fs.Bool(flagReverse, false, "results are sorted in descending order") + + return fs +} + +func getPagination(cmd *cobra.Command) (*query.PageRequest, error) { + var ( + pageKey, _ = cmd.Flags().GetString(flagPageKey) + offset, _ = cmd.Flags().GetUint64(flagOffset) + limit, _ = cmd.Flags().GetUint64(flagLimit) + countTotal, _ = cmd.Flags().GetBool(flagCountTotal) + page, _ = cmd.Flags().GetUint64(flagPage) + reverse, _ = cmd.Flags().GetBool(flagReverse) + ) + + if page > 1 && offset > 0 { + return nil, errors.New("page and offset cannot be used together") + } + + if page > 1 { + offset = (page - 1) * limit + } + + return &query.PageRequest{ + Key: []byte(pageKey), + Offset: offset, + Limit: limit, + CountTotal: countTotal, + Reverse: reverse, + }, nil +} diff --git a/ignite/cmd/node_query_bank.go b/ignite/cmd/node_query_bank.go new file mode 100644 index 0000000000..9ebf9cc287 --- /dev/null +++ b/ignite/cmd/node_query_bank.go @@ -0,0 +1,14 @@ +package ignitecmd + +import "github.com/spf13/cobra" + +func NewNodeQueryBank() *cobra.Command { + c := &cobra.Command{ + Use: "bank", + Short: "Querying commands for the bank module", + } + + c.AddCommand(NewNodeQueryBankBalances()) + + return c +} diff --git a/ignite/cmd/node_query_bank_balances.go b/ignite/cmd/node_query_bank_balances.go new file mode 100644 index 0000000000..8aa4acbc96 --- /dev/null +++ b/ignite/cmd/node_query_bank_balances.go @@ -0,0 +1,61 @@ +package ignitecmd + +import ( + "fmt" + + "github.com/ignite/cli/ignite/pkg/cliui" + "github.com/spf13/cobra" +) + +func NewNodeQueryBankBalances() *cobra.Command { + c := &cobra.Command{ + Use: "balances [from_account_or_address]", + Short: "Query for account balances by account name or address", + RunE: nodeQueryBankBalancesHandler, + Args: cobra.ExactArgs(1), + } + + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetAccountPrefixes()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) + c.Flags().AddFlagSet(flagSetPagination("all balances")) + + return c +} + +func nodeQueryBankBalancesHandler(cmd *cobra.Command, args []string) error { + inputAccount := args[0] + + client, err := newNodeCosmosClient(cmd) + if err != nil { + return err + } + + // inputAccount can be an account of the keyring or a raw address + address, err := client.Address(inputAccount) + if err != nil { + address = inputAccount + } + + pagination, err := getPagination(cmd) + if err != nil { + return err + } + + session := cliui.New() + defer session.Cleanup() + session.StartSpinner("Querying...") + balances, err := client.BankBalances(cmd.Context(), address, pagination) + if err != nil { + return err + } + + var rows [][]string + for _, b := range balances { + rows = append(rows, []string{fmt.Sprintf("%s", b.Amount), b.Denom}) + } + + session.StopSpinner() + return session.PrintTable([]string{"Amount", "Denom"}, rows...) +} diff --git a/ignite/cmd/node_query_tx.go b/ignite/cmd/node_query_tx.go new file mode 100644 index 0000000000..39d84ee0b7 --- /dev/null +++ b/ignite/cmd/node_query_tx.go @@ -0,0 +1,41 @@ +package ignitecmd + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" +) + +func NewNodeQueryTx() *cobra.Command { + c := &cobra.Command{ + Use: "tx [hash]", + Short: "Query for transaction by hash", + RunE: nodeQueryTxHandler, + Args: cobra.ExactArgs(1), + } + return c +} + +func nodeQueryTxHandler(cmd *cobra.Command, args []string) error { + bz, err := hex.DecodeString(args[0]) + if err != nil { + return err + } + rpc, err := sdkclient.NewClientFromNode(getRPC(cmd)) + if err != nil { + return err + } + resp, err := rpc.Tx(cmd.Context(), bz, false) + if err != nil { + return err + } + bz, err = json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + fmt.Println(string(bz)) + return nil +} diff --git a/ignite/cmd/node_tx.go b/ignite/cmd/node_tx.go new file mode 100644 index 0000000000..006bec27d2 --- /dev/null +++ b/ignite/cmd/node_tx.go @@ -0,0 +1,76 @@ +package ignitecmd + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" +) + +const ( + flagGenerateOnly = "generate-only" + + gasFlagAuto = "auto" + flagGasPrices = "gas-prices" + flagGas = "gas" + flagFees = "fees" + flagBroadcastMode = "broadcast-mode" +) + +func NewNodeTx() *cobra.Command { + c := &cobra.Command{ + Use: "tx", + Short: "Transactions subcommands", + } + c.PersistentFlags().AddFlagSet(flagSetHome()) + c.PersistentFlags().AddFlagSet(flagSetKeyringBackend()) + c.PersistentFlags().AddFlagSet(flagSetAccountPrefixes()) + c.PersistentFlags().AddFlagSet(flagSetKeyringDir()) + c.PersistentFlags().AddFlagSet(flagSetGenerateOnly()) + c.PersistentFlags().AddFlagSet(flagSetGasFlags()) + c.PersistentFlags().String(flagFees, "", "Fees to pay along with transaction; eg: 10uatom") + c.PersistentFlags().String(flagBroadcastMode, flags.BroadcastBlock, "Transaction broadcasting mode (sync|async|block), use sync if you encounter timeouts") + + c.AddCommand(NewNodeTxBank()) + + return c +} + +func flagSetGenerateOnly() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Bool(flagGenerateOnly, false, "Build an unsigned transaction and write it to STDOUT") + return fs +} + +func getGenerateOnly(cmd *cobra.Command) bool { + generateOnly, _ := cmd.Flags().GetBool(flagGenerateOnly) + return generateOnly +} + +func flagSetGasFlags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.String(flagGasPrices, "", "Gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom)") + fs.String(flagGas, gasFlagAuto, fmt.Sprintf("gas limit to set per-transaction; set to %q to calculate sufficient gas automatically", gasFlagAuto)) + return fs +} + +func getGasPrices(cmd *cobra.Command) string { + gasPrices, _ := cmd.Flags().GetString(flagGasPrices) + return gasPrices +} + +func getGas(cmd *cobra.Command) string { + gas, _ := cmd.Flags().GetString(flagGas) + return gas +} + +func getFees(cmd *cobra.Command) string { + fees, _ := cmd.Flags().GetString(flagFees) + return fees +} + +func getBroadcastMode(cmd *cobra.Command) string { + broadcastMode, _ := cmd.Flags().GetString(flagBroadcastMode) + return broadcastMode +} diff --git a/ignite/cmd/node_tx_bank.go b/ignite/cmd/node_tx_bank.go new file mode 100644 index 0000000000..9c541a5b36 --- /dev/null +++ b/ignite/cmd/node_tx_bank.go @@ -0,0 +1,14 @@ +package ignitecmd + +import "github.com/spf13/cobra" + +func NewNodeTxBank() *cobra.Command { + c := &cobra.Command{ + Use: "bank", + Short: "Bank transaction subcommands", + } + + c.AddCommand(NewNodeTxBankSend()) + + return c +} diff --git a/ignite/cmd/node_tx_bank_send.go b/ignite/cmd/node_tx_bank_send.go new file mode 100644 index 0000000000..5665c6f690 --- /dev/null +++ b/ignite/cmd/node_tx_bank_send.go @@ -0,0 +1,83 @@ +package ignitecmd + +import ( + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ignite/cli/ignite/pkg/cliui" + "github.com/spf13/cobra" +) + +func NewNodeTxBankSend() *cobra.Command { + c := &cobra.Command{ + Use: "send [from_account_or_address] [to_account_or_address] [amount]", + Short: "Send funds from one account to another.", + RunE: nodeTxBankSendHandler, + Args: cobra.ExactArgs(3), + } + + return c +} + +func nodeTxBankSendHandler(cmd *cobra.Command, args []string) error { + var ( + fromAccountInput = args[0] + toAccountInput = args[1] + amount = args[2] + generateOnly = getGenerateOnly(cmd) + ) + + client, err := newNodeCosmosClient(cmd) + if err != nil { + return err + } + + // fromAccountInput must be an account of the keyring + fromAccount, err := client.Account(fromAccountInput) + if err != nil { + return err + } + + // toAccountInput can be an account of the keyring or a raw address + toAddress, err := client.Address(toAccountInput) + if err != nil { + toAddress = toAccountInput + } + + coins, err := sdk.ParseCoinsNormalized(amount) + if err != nil { + return err + } + + tx, err := client.BankSendTx(fromAccount, toAddress, coins) + if err != nil { + return err + } + + session := cliui.New() + defer session.Cleanup() + if generateOnly { + json, err := tx.EncodeJSON() + if err != nil { + return err + } + + session.StopSpinner() + return session.Println(string(json)) + } + + session.StartSpinner("Sending transaction...") + resp, err := tx.Broadcast() + if err != nil { + return err + } + + session.StopSpinner() + session.Printf("Transaction broadcast successful! (hash = %s)\n", resp.TxHash) + session.Printf("%s sent from %s to %s\n", amount, fromAccountInput, toAccountInput) + if getBroadcastMode(cmd) != flags.BroadcastBlock { + session.Println("Transaction waiting to be included in a block.") + session.Println("Run the following command to follow the transaction status:") + session.Printf(" ignite node --node %s q tx %s\n", getRPC(cmd), resp.TxHash) + } + return nil +} diff --git a/ignite/cmd/relayer_configure.go b/ignite/cmd/relayer_configure.go index 17b731e10f..0e59a43b9b 100644 --- a/ignite/cmd/relayer_configure.go +++ b/ignite/cmd/relayer_configure.go @@ -85,6 +85,7 @@ func NewRelayerConfigure() *cobra.Command { c.Flags().String(flagSourceClientID, "", "use a custom client id for source") c.Flags().String(flagTargetClientID, "", "use a custom client id for target") c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } @@ -99,6 +100,7 @@ func relayerConfigureHandler(cmd *cobra.Command, args []string) (err error) { ca, err := cosmosaccount.New( cosmosaccount.WithKeyringBackend(getKeyringBackend(cmd)), + cosmosaccount.WithHome(getKeyringDir(cmd)), ) if err != nil { return err diff --git a/ignite/cmd/relayer_connect.go b/ignite/cmd/relayer_connect.go index ebd2d0250a..74b3fe6be8 100644 --- a/ignite/cmd/relayer_connect.go +++ b/ignite/cmd/relayer_connect.go @@ -23,6 +23,7 @@ func NewRelayerConnect() *cobra.Command { } c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetKeyringDir()) return c } @@ -37,6 +38,7 @@ func relayerConnectHandler(cmd *cobra.Command, args []string) (err error) { ca, err := cosmosaccount.New( cosmosaccount.WithKeyringBackend(getKeyringBackend(cmd)), + cosmosaccount.WithHome(getKeyringDir(cmd)), ) if err != nil { return err diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index b9df53d33c..7d8c25b5f0 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -3,6 +3,7 @@ package chaincmd import ( "fmt" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/cosmosver" ) @@ -50,7 +51,6 @@ const ( constTendermint = "tendermint" constJSON = "json" - constSync = "sync" ) type KeyringBackend string @@ -524,8 +524,7 @@ func (c ChainCmd) BankSendCommand(fromAddress, toAddress, amount string) step.Op fromAddress, toAddress, amount, - optionBroadcastMode, - constSync, + optionBroadcastMode, flags.BroadcastSync, optionYes, ) diff --git a/ignite/pkg/cmdrunner/cmdrunner.go b/ignite/pkg/cmdrunner/cmdrunner.go index ed3cfe28b8..d1574334d9 100644 --- a/ignite/pkg/cmdrunner/cmdrunner.go +++ b/ignite/pkg/cmdrunner/cmdrunner.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/exec" + "strings" "golang.org/x/sync/errgroup" @@ -21,6 +22,7 @@ type Runner struct { stdin io.Reader workdir string runParallel bool + debug bool } // Option defines option to run commands @@ -68,6 +70,12 @@ func EndSignal(s os.Signal) Option { } } +func EnableDebug() Option { + return func(r *Runner) { + r.debug = true + } +} + // New returns a new commands runner func New(options ...Option) *Runner { runner := &Runner{ @@ -85,9 +93,13 @@ func (r *Runner) Run(ctx context.Context, steps ...*step.Step) error { return nil } g, ctx := errgroup.WithContext(ctx) - for _, step := range steps { + for i, step := range steps { // copy s to a new variable to allocate a new address // so we can safely use it inside goroutines spawned in this loop. + if r.debug { + fmt.Printf("Step %d: %s %s\n", i, step.Exec.Command, + strings.Join(step.Exec.Args, " ")) + } step := step if err := ctx.Err(); err != nil { return err diff --git a/ignite/pkg/cosmosaccount/cosmosaccount.go b/ignite/pkg/cosmosaccount/cosmosaccount.go index 9f6b437058..aa7579abdd 100644 --- a/ignite/pkg/cosmosaccount/cosmosaccount.go +++ b/ignite/pkg/cosmosaccount/cosmosaccount.go @@ -146,7 +146,7 @@ func (a Account) Address(accPrefix string) string { panic(err) } - return toBench32(accPrefix, pk.Address()) + return toBech32(accPrefix, pk.Address()) } // PubKey returns a public key for account. @@ -160,7 +160,7 @@ func (a Account) PubKey() string { return pk.String() } -func toBench32(prefix string, addr []byte) string { +func toBech32(prefix string, addr []byte) string { bech32Addr, err := bech32.ConvertAndEncode(prefix, addr) if err != nil { panic(err) @@ -289,6 +289,25 @@ func (r Registry) GetByName(name string) (Account, error) { return acc, nil } +// GetByAddress returns an account by its address. +func (r Registry) GetByAddress(address string) (Account, error) { + sdkAddr, err := sdktypes.AccAddressFromBech32(address) + if err != nil { + return Account{}, err + } + record, err := r.Keyring.KeyByAddress(sdkAddr) + if errors.Is(err, dkeyring.ErrKeyNotFound) || errors.Is(err, sdkerrors.ErrKeyNotFound) { + return Account{}, &AccountDoesNotExistError{address} + } + if err != nil { + return Account{}, err + } + return Account{ + Name: record.Name, + Record: record, + }, nil +} + // List lists all accounts. func (r Registry) List() ([]Account, error) { records, err := r.Keyring.List() diff --git a/ignite/pkg/cosmosaccount/cosmosaccount_test.go b/ignite/pkg/cosmosaccount/cosmosaccount_test.go new file mode 100644 index 0000000000..4f5065e9bc --- /dev/null +++ b/ignite/pkg/cosmosaccount/cosmosaccount_test.go @@ -0,0 +1,71 @@ +package cosmosaccount_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/pkg/cosmosaccount" +) + +const testAccountName = "myTestAccount" + +func TestRegistry(t *testing.T) { + tmpDir := t.TempDir() + registry, err := cosmosaccount.New(cosmosaccount.WithHome(tmpDir)) + require.NoError(t, err) + + account, mnemonic, err := registry.Create(testAccountName) + require.NoError(t, err) + require.Equal(t, testAccountName, account.Name) + require.NotEmpty(t, account.Record.PubKey.Value) + + getAccount, err := registry.GetByName(testAccountName) + require.NoError(t, err) + require.Equal(t, getAccount, account) + + sdkaddr, _ := account.Record.GetAddress() + addr := sdkaddr.String() + getAccount, err = registry.GetByAddress(addr) + require.NoError(t, err) + require.Equal(t, getAccount.Record.PubKey, account.Record.PubKey) + require.Equal(t, getAccount.Name, testAccountName) + require.Equal(t, getAccount.Name, account.Name) + require.Equal(t, getAccount.Name, account.Record.Name) + + addr = account.Address("cosmos") + getAccount, err = registry.GetByAddress(addr) + require.NoError(t, err) + require.Equal(t, getAccount.Record.PubKey, account.Record.PubKey) + require.Equal(t, getAccount.Name, testAccountName) + require.Equal(t, getAccount.Name, account.Name) + require.Equal(t, getAccount.Name, account.Record.Name) + + secondTmpDir := t.TempDir() + secondRegistry, err := cosmosaccount.New(cosmosaccount.WithHome(secondTmpDir)) + require.NoError(t, err) + + importedAccount, err := secondRegistry.Import(testAccountName, mnemonic, "") + require.NoError(t, err) + require.Equal(t, testAccountName, importedAccount.Name) + require.Equal(t, importedAccount.Record.PubKey, account.Record.PubKey) + + _, _, err = registry.Create("another one") + require.NoError(t, err) + list, err := registry.List() + require.NoError(t, err) + require.Equal(t, 2, len(list)) + + err = registry.DeleteByName(testAccountName) + require.NoError(t, err) + afterDeleteList, err := registry.List() + require.NoError(t, err) + require.Equal(t, 1, len(afterDeleteList)) + + _, err = registry.GetByName(testAccountName) + var expectedErr *cosmosaccount.AccountDoesNotExistError + require.ErrorAs(t, err, &expectedErr) + + _, err = registry.GetByAddress(addr) + require.ErrorAs(t, err, &expectedErr) +} diff --git a/ignite/pkg/cosmosclient/bank.go b/ignite/pkg/cosmosclient/bank.go new file mode 100644 index 0000000000..46f13fd9d2 --- /dev/null +++ b/ignite/pkg/cosmosclient/bank.go @@ -0,0 +1,35 @@ +package cosmosclient + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ignite/cli/ignite/pkg/cosmosaccount" +) + +func (c Client) BankBalances(ctx context.Context, address string, pagination *query.PageRequest) (sdk.Coins, error) { + defer c.lockBech32Prefix()() + + req := &banktypes.QueryAllBalancesRequest{ + Address: address, + Pagination: pagination, + } + + resp, err := banktypes.NewQueryClient(c.context).AllBalances(ctx, req) + if err != nil { + return nil, err + } + return resp.Balances, nil +} + +func (c Client) BankSendTx(fromAccount cosmosaccount.Account, toAddress string, amount sdk.Coins) (TxService, error) { + msg := &banktypes.MsgSend{ + FromAddress: fromAccount.Address(c.addressPrefix), + ToAddress: toAddress, + Amount: amount, + } + + return c.CreateTx(fromAccount, msg) +} diff --git a/ignite/pkg/cosmosclient/consensus.go b/ignite/pkg/cosmosclient/consensus.go new file mode 100644 index 0000000000..6baffcf167 --- /dev/null +++ b/ignite/pkg/cosmosclient/consensus.go @@ -0,0 +1,66 @@ +package cosmosclient + +import ( + "context" + "encoding/base64" + "time" + + commitmenttypes "github.com/cosmos/ibc-go/v5/modules/core/23-commitment/types" + "github.com/tendermint/tendermint/libs/bytes" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// ConsensusInfo is the validator consensus info +type ConsensusInfo struct { + Timestamp string `json:"Timestamp"` + Root string `json:"Root"` + NextValidatorsHash string `json:"NextValidatorsHash"` + ValidatorSet *tmproto.ValidatorSet `json:"ValidatorSet"` +} + +// ConsensusInfo returns the appropriate tendermint consensus state by given height +// and the validator set for the next height +func (c Client) ConsensusInfo(ctx context.Context, height int64) (ConsensusInfo, error) { + node, err := c.Context().GetNode() + if err != nil { + return ConsensusInfo{}, err + } + + commit, err := node.Commit(ctx, &height) + if err != nil { + return ConsensusInfo{}, err + } + + var ( + page = 1 + count = 10_000 + ) + validators, err := node.Validators(ctx, &height, &page, &count) + if err != nil { + return ConsensusInfo{}, err + } + + protoValset, err := tmtypes.NewValidatorSet(validators.Validators).ToProto() + if err != nil { + return ConsensusInfo{}, err + } + + heightNext := height + 1 + validatorsNext, err := node.Validators(ctx, &heightNext, &page, &count) + if err != nil { + return ConsensusInfo{}, err + } + + var ( + hash = tmtypes.NewValidatorSet(validatorsNext.Validators).Hash() + root = commitmenttypes.NewMerkleRoot(commit.AppHash) + ) + + return ConsensusInfo{ + Timestamp: commit.Time.Format(time.RFC3339Nano), + NextValidatorsHash: bytes.HexBytes(hash).String(), + Root: base64.StdEncoding.EncodeToString(root.Hash), + ValidatorSet: protoValset, + }, nil +} diff --git a/ignite/pkg/cosmosclient/cosmosclient.go b/ignite/pkg/cosmosclient/cosmosclient.go index 45ebd2706d..dfba365b1c 100644 --- a/ignite/pkg/cosmosclient/cosmosclient.go +++ b/ignite/pkg/cosmosclient/cosmosclient.go @@ -3,12 +3,12 @@ package cosmosclient import ( "context" - "encoding/base64" "encoding/hex" "fmt" "io" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -21,21 +21,16 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdktypes "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/tx/signing" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" staking "github.com/cosmos/cosmos-sdk/x/staking/types" - commitmenttypes "github.com/cosmos/ibc-go/v5/modules/core/23-commitment/types" "github.com/gogo/protobuf/proto" prototypes "github.com/gogo/protobuf/types" "github.com/pkg/errors" - "github.com/tendermint/tendermint/libs/bytes" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" rpchttp "github.com/tendermint/tendermint/rpc/client/http" ctypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" "github.com/ignite/cli/ignite/pkg/cosmosaccount" "github.com/ignite/cli/ignite/pkg/cosmosfaucet" @@ -87,6 +82,12 @@ type Client struct { homePath string keyringServiceName string keyringBackend cosmosaccount.KeyringBackend + keyringDir string + + gas string + gasPrices string + fees string + broadcastMode string } // Option configures your client. @@ -116,6 +117,13 @@ func WithKeyringBackend(backend cosmosaccount.KeyringBackend) Option { } } +// WithKeyringDir sets the directory of the keyring. By default, it uses cosmosaccount.KeyringHome +func WithKeyringDir(keyringDir string) Option { + return func(c *Client) { + c.keyringDir = keyringDir + } +} + // WithNodeAddress sets the node address of your chain. When this option is not provided // `http://localhost:26657` is used as default. func WithNodeAddress(addr string) Option { @@ -143,6 +151,35 @@ func WithUseFaucet(faucetAddress, denom string, minAmount uint64) Option { } } +// WithGas sets an explicit gas-limit on transactions. +// Set to "auto" to calculate automatically +func WithGas(gas string) Option { + return func(c *Client) { + c.gas = gas + } +} + +// WithGasPrices sets the price per gas (e.g. 0.1uatom) +func WithGasPrices(gasPrices string) Option { + return func(c *Client) { + c.gasPrices = gasPrices + } +} + +// WithFees sets the fees (e.g. 10uatom) +func WithFees(fees string) Option { + return func(c *Client) { + c.fees = fees + } +} + +// WithBroadcastMode sets the broadcast mode +func WithBroadcastMode(broadcastMode string) Option { + return func(c *Client) { + c.broadcastMode = broadcastMode + } +} + // New creates a new client with given options. func New(ctx context.Context, options ...Option) (Client, error) { c := Client{ @@ -153,6 +190,8 @@ func New(ctx context.Context, options ...Option) (Client, error) { faucetDenom: defaultFaucetDenom, faucetMinAmount: defaultFaucetMinAmount, out: io.Discard, + gas: strconv.Itoa(defaultGasLimit), + broadcastMode: flags.BroadcastBlock, } var err error @@ -180,16 +219,20 @@ func New(ctx context.Context, options ...Option) (Client, error) { c.homePath = filepath.Join(home, "."+c.chainID) } + if c.keyringDir == "" { + c.keyringDir = c.homePath + } + c.AccountRegistry, err = cosmosaccount.New( cosmosaccount.WithKeyringServiceName(c.keyringServiceName), cosmosaccount.WithKeyringBackend(c.keyringBackend), - cosmosaccount.WithHome(c.homePath), + cosmosaccount.WithHome(c.keyringDir), ) if err != nil { return Client{}, err } - c.context = newContext(c.RPC, c.out, c.chainID, c.homePath).WithKeyring(c.AccountRegistry.Keyring) + c.context = c.newContext() c.Factory = newFactory(c.context) // set address prefix in SDK global config @@ -198,18 +241,83 @@ func New(ctx context.Context, options ...Option) (Client, error) { return c, nil } -func (c Client) Account(accountName string) (cosmosaccount.Account, error) { - return c.AccountRegistry.GetByName(accountName) +// LatestBlockHeight returns the lastest block height of the app. +func (c Client) LatestBlockHeight() (int64, error) { + resp, err := c.Status(context.Background()) + if err != nil { + return 0, err + } + return resp.SyncInfo.LatestBlockHeight, nil } -// Address returns the account address from account name. -func (c Client) Address(accountName string) (sdktypes.AccAddress, error) { - account, err := c.Account(accountName) +// WaitForNextBlock waits until next block is committed. +// It reads the current block height and then waits for another block to be +// committed. +func (c Client) WaitForNextBlock() error { + return c.WaitForNBlocks(1) +} + +// WaitForNBlocks reads the current block height and then waits for anothers n +// blocks to be committed. +func (c Client) WaitForNBlocks(n int64) error { + start, err := c.LatestBlockHeight() if err != nil { - return sdktypes.AccAddress{}, err + return err + } + return c.WaitForBlockHeight(start + n) +} + +// WaitForBlockHeight waits until block height h is committed, or return an +// error if a timeout is reached (10s). +func (c Client) WaitForBlockHeight(h int64) error { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + timeout := time.After(10 * time.Second) + + for { + status, err := c.RPC.Status(context.Background()) + if err != nil { + return err + } + if status.SyncInfo.LatestBlockHeight >= h { + return nil + } + select { + case <-timeout: + return errors.New("timeout exceeded waiting for block") + case <-ticker.C: + } } +} + +// Account returns the account with name or address equal to nameOrAddress. +func (c Client) Account(nameOrAddress string) (cosmosaccount.Account, error) { + defer c.lockBech32Prefix()() - return account.Record.GetAddress() + return c.account(nameOrAddress) +} + +func (c Client) account(nameOrAddress string) (cosmosaccount.Account, error) { + a, err := c.AccountRegistry.GetByName(nameOrAddress) + if err == nil { + return a, nil + } + return c.AccountRegistry.GetByAddress(nameOrAddress) +} + +// Address returns the account address from account name. +func (c Client) Address(accountName string) (string, error) { + defer c.lockBech32Prefix()() + + account, err := c.account(accountName) + if err != nil { + return "", err + } + sdkaddr, err := account.Record.GetAddress() + if err != nil { + return "", err + } + return sdkaddr.String(), nil } // Context returns client context @@ -275,61 +383,6 @@ func (r Response) Decode(message proto.Message) error { TypeUrl: resData.TypeUrl, Value: resData.Value, }, message) - -} - -// ConsensusInfo is the validator consensus info -type ConsensusInfo struct { - Timestamp string `json:"Timestamp"` - Root string `json:"Root"` - NextValidatorsHash string `json:"NextValidatorsHash"` - ValidatorSet *tmproto.ValidatorSet `json:"ValidatorSet"` -} - -// ConsensusInfo returns the appropriate tendermint consensus state by given height -// and the validator set for the next height -func (c Client) ConsensusInfo(ctx context.Context, height int64) (ConsensusInfo, error) { - node, err := c.Context().GetNode() - if err != nil { - return ConsensusInfo{}, err - } - - commit, err := node.Commit(ctx, &height) - if err != nil { - return ConsensusInfo{}, err - } - - var ( - page = 1 - count = 10_000 - ) - validators, err := node.Validators(ctx, &height, &page, &count) - if err != nil { - return ConsensusInfo{}, err - } - - protoValset, err := tmtypes.NewValidatorSet(validators.Validators).ToProto() - if err != nil { - return ConsensusInfo{}, err - } - - heightNext := height + 1 - validatorsNext, err := node.Validators(ctx, &heightNext, &page, &count) - if err != nil { - return ConsensusInfo{}, err - } - - var ( - hash = tmtypes.NewValidatorSet(validatorsNext.Validators).Hash() - root = commitmenttypes.NewMerkleRoot(commit.AppHash) - ) - - return ConsensusInfo{ - Timestamp: commit.Time.Format(time.RFC3339Nano), - NextValidatorsHash: bytes.HexBytes(hash).String(), - Root: base64.StdEncoding.EncodeToString(root.Hash), - ValidatorSet: protoValset, - }, nil } // Status returns the node status @@ -337,81 +390,76 @@ func (c Client) Status(ctx context.Context) (*ctypes.ResultStatus, error) { return c.RPC.Status(ctx) } -// BroadcastTx creates and broadcasts a tx with given messages for account. -func (c Client) BroadcastTx(accountName string, msgs ...sdktypes.Msg) (Response, error) { - _, broadcast, err := c.BroadcastTxWithProvision(accountName, msgs...) - if err != nil { - return Response{}, err - } - return broadcast() -} - // protects sdktypes.Config. var mconf sync.Mutex -func (c Client) BroadcastTxWithProvision(accountName string, msgs ...sdktypes.Msg) ( - gas uint64, broadcast func() (Response, error), err error, -) { - if err := c.prepareBroadcast(context.Background(), accountName, msgs); err != nil { - return 0, nil, err +func (c Client) lockBech32Prefix() (unlockFn func()) { + mconf.Lock() + config := sdktypes.GetConfig() + config.SetBech32PrefixForAccount(c.addressPrefix, c.addressPrefix+"pub") + return mconf.Unlock +} + +func (c Client) BroadcastTx(account cosmosaccount.Account, msgs ...sdktypes.Msg) (Response, error) { + txService, err := c.CreateTx(account, msgs...) + if err != nil { + return Response{}, err } - // set address prefix in SDK global config - c.SetConfigAddressPrefix() + return txService.Broadcast() +} - accountAddress, err := c.Address(accountName) +func (c Client) CreateTx(account cosmosaccount.Account, msgs ...sdktypes.Msg) (TxService, error) { + defer c.lockBech32Prefix()() + + sdkaddr, err := account.Record.GetAddress() if err != nil { - return 0, nil, err + return TxService{}, err } ctx := c.context. - WithFromName(accountName). - WithFromAddress(accountAddress) + WithFromName(account.Name). + WithFromAddress(sdkaddr) txf, err := prepareFactory(ctx, c.Factory) if err != nil { - return 0, nil, err + return TxService{}, err } - _, gas, err = tx.CalculateGas(ctx, txf, msgs...) - if err != nil { - return 0, nil, err - } - // the simulated gas can vary from the actual gas needed for a real transaction - // we add an additional amount to endure sufficient gas is provided - gas += 10000 - txf = txf.WithGas(gas) - - // Return the provision function - return gas, func() (Response, error) { - txUnsigned, err := txf.BuildUnsignedTx(msgs...) + var gas uint64 + if c.gas != "" && c.gas != "auto" { + gas, err = strconv.ParseUint(c.gas, 10, 64) if err != nil { - return Response{}, err - } - - txUnsigned.SetFeeGranter(ctx.GetFeeGranterAddress()) - if err := tx.Sign(txf, accountName, txUnsigned, true); err != nil { - return Response{}, err + return TxService{}, err } - - txBytes, err := ctx.TxConfig.TxEncoder()(txUnsigned.GetTx()) + } else { + _, gas, err = tx.CalculateGas(ctx, txf, msgs...) if err != nil { - return Response{}, err + return TxService{}, err } + // the simulated gas can vary from the actual gas needed for a real transaction + // we add an additional amount to ensure sufficient gas is provided + gas += 20000 + } + txf = txf.WithGas(gas) + txf = txf.WithFees(c.fees) - resp, err := ctx.BroadcastTx(txBytes) - if err == sdkerrors.ErrInsufficientFunds { - err = c.makeSureAccountHasTokens(context.Background(), accountAddress.String()) - if err != nil { - return Response{}, err - } - resp, err = ctx.BroadcastTx(txBytes) - } + if c.gasPrices != "" { + txf = txf.WithGasPrices(c.gasPrices) + } + + txUnsigned, err := txf.BuildUnsignedTx(msgs...) + if err != nil { + return TxService{}, err + } + + txUnsigned.SetFeeGranter(ctx.GetFeeGranterAddress()) - return Response{ - Codec: ctx.Codec, - TxResponse: resp, - }, handleBroadcastResult(resp, err) + return TxService{ + client: c, + clientContext: ctx, + txBuilder: txUnsigned, + txFactory: txf, }, nil } @@ -425,7 +473,7 @@ func (c *Client) prepareBroadcast(ctx context.Context, accountName string, _ []s // } // } - account, err := c.Account(accountName) + account, err := c.account(accountName) if err != nil { return err } @@ -486,14 +534,14 @@ func (c *Client) checkAccountBalance(ctx context.Context, address string) error func handleBroadcastResult(resp *sdktypes.TxResponse, err error) error { if err != nil { if strings.Contains(err.Error(), "not found") { - return errors.New("make sure that your SPN account has enough balance") + return errors.New("make sure that your account has enough balance") } return err } if resp.Code > 0 { - return fmt.Errorf("SPN error with '%d' code: %s", resp.Code, resp.RawLog) + return fmt.Errorf("error code: '%d' msg: '%s'", resp.Code, resp.RawLog) } return nil } @@ -524,12 +572,7 @@ func prepareFactory(clientCtx client.Context, txf tx.Factory) (tx.Factory, error return txf, nil } -func newContext( - c *rpchttp.HTTP, - out io.Writer, - chainID, - home string, -) client.Context { +func (c Client) newContext() client.Context { var ( amino = codec.NewLegacyAmino() interfaceRegistry = codectypes.NewInterfaceRegistry() @@ -542,20 +585,22 @@ func newContext( sdktypes.RegisterInterfaces(interfaceRegistry) staking.RegisterInterfaces(interfaceRegistry) cryptocodec.RegisterInterfaces(interfaceRegistry) + banktypes.RegisterInterfaces(interfaceRegistry) return client.Context{}. - WithChainID(chainID). + WithChainID(c.chainID). WithInterfaceRegistry(interfaceRegistry). WithCodec(marshaler). WithTxConfig(txConfig). WithLegacyAmino(amino). WithInput(os.Stdin). - WithOutput(out). + WithOutput(c.out). WithAccountRetriever(authtypes.AccountRetriever{}). - WithBroadcastMode(flags.BroadcastBlock). - WithHomeDir(home). - WithClient(c). - WithSkipConfirmation(true) + WithBroadcastMode(c.broadcastMode). + WithHomeDir(c.homePath). + WithClient(c.RPC). + WithSkipConfirmation(true). + WithKeyring(c.AccountRegistry.Keyring) } func newFactory(clientCtx client.Context) tx.Factory { diff --git a/ignite/pkg/cosmosclient/txservice.go b/ignite/pkg/cosmosclient/txservice.go new file mode 100644 index 0000000000..c702970e7c --- /dev/null +++ b/ignite/pkg/cosmosclient/txservice.go @@ -0,0 +1,63 @@ +package cosmosclient + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdktypes "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +type TxService struct { + client Client + clientContext client.Context + txBuilder client.TxBuilder + txFactory tx.Factory +} + +// Gas is gas decided to use for this tx. +// either calculated or configured by the caller. +func (s TxService) Gas() uint64 { + return s.txBuilder.GetTx().GetGas() +} + +// Broadcast signs and broadcasts this tx. +func (s TxService) Broadcast() (Response, error) { + defer s.client.lockBech32Prefix()() + + accountName := s.clientContext.GetFromName() + accountAddress := s.clientContext.GetFromAddress() + + if err := s.client.prepareBroadcast(context.Background(), accountName, []sdktypes.Msg{}); err != nil { + return Response{}, err + } + + if err := tx.Sign(s.txFactory, accountName, s.txBuilder, true); err != nil { + return Response{}, err + } + + txBytes, err := s.clientContext.TxConfig.TxEncoder()(s.txBuilder.GetTx()) + if err != nil { + return Response{}, err + } + + resp, err := s.clientContext.BroadcastTx(txBytes) + if err == sdkerrors.ErrInsufficientFunds { + err = s.client.makeSureAccountHasTokens(context.Background(), accountAddress.String()) + if err != nil { + return Response{}, err + } + resp, err = s.clientContext.BroadcastTx(txBytes) + } + + return Response{ + Codec: s.clientContext.Codec, + TxResponse: resp, + }, handleBroadcastResult(resp, err) +} + +// EncodeJSON encodes the transaction as a json string +func (s TxService) EncodeJSON() ([]byte, error) { + return s.client.context.TxConfig.TxJSONEncoder()(s.txBuilder.GetTx()) +} diff --git a/ignite/services/network/campaign.go b/ignite/services/network/campaign.go index b32d5a79a7..d674a38ba9 100644 --- a/ignite/services/network/campaign.go +++ b/ignite/services/network/campaign.go @@ -88,7 +88,7 @@ func (n Network) CreateCampaign(name, metadata string, totalSupply sdk.Coins) (u totalSupply, []byte(metadata), ) - res, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateCampaign) + res, err := n.cosmos.BroadcastTx(n.account, msgCreateCampaign) if err != nil { return 0, err } @@ -117,7 +117,7 @@ func (n Network) InitializeMainnet( mainnetChainID, ) - res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + res, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return 0, err } @@ -162,7 +162,7 @@ func (n Network) UpdateCampaign( )) } - if _, err := n.cosmos.BroadcastTx(n.account.Name, msgs...); err != nil { + if _, err := n.cosmos.BroadcastTx(n.account, msgs...); err != nil { return err } n.ev.Send(events.New(events.StatusDone, fmt.Sprintf( diff --git a/ignite/services/network/client.go b/ignite/services/network/client.go index 05fd8b3538..a5ed289ac8 100644 --- a/ignite/services/network/client.go +++ b/ignite/services/network/client.go @@ -25,7 +25,7 @@ func (n Network) CreateClient( rewardsInfo.RevisionHeight, ) - res, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateClient) + res, err := n.cosmos.BroadcastTx(n.account, msgCreateClient) if err != nil { return "", err } diff --git a/ignite/services/network/join.go b/ignite/services/network/join.go index efef9fc838..9c3bae2b20 100644 --- a/ignite/services/network/join.go +++ b/ignite/services/network/join.go @@ -113,7 +113,7 @@ func (n Network) sendValidatorRequest( n.ev.Send(events.New(events.StatusOngoing, "Broadcasting validator transaction")) - res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + res, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return err } diff --git a/ignite/services/network/join_test.go b/ignite/services/network/join_test.go index e0aece3732..b4e43b413b 100644 --- a/ignite/services/network/join_test.go +++ b/ignite/services/network/join_test.go @@ -42,7 +42,7 @@ func TestJoin(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgRequestAddValidator{ Creator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, @@ -93,7 +93,7 @@ func TestJoin(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgRequestAddValidator{ Creator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, @@ -140,7 +140,7 @@ func TestJoin(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgRequestAddValidator{ Creator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, @@ -193,7 +193,7 @@ func TestJoin(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgRequestAddValidator{ Creator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, @@ -217,7 +217,7 @@ func TestJoin(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgRequestAddAccount{ Creator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, diff --git a/ignite/services/network/launch.go b/ignite/services/network/launch.go index 83c0b7204d..782750e68a 100644 --- a/ignite/services/network/launch.go +++ b/ignite/services/network/launch.go @@ -50,7 +50,7 @@ func (n Network) TriggerLaunch(ctx context.Context, launchID uint64, remainingTi msg := launchtypes.NewMsgTriggerLaunch(address, launchID, int64(remainingTime.Seconds())) n.ev.Send(events.New(events.StatusOngoing, "Setting launch time")) - res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + res, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return err } @@ -72,7 +72,7 @@ func (n Network) RevertLaunch(launchID uint64, chain Chain) error { address := n.account.Address(networktypes.SPN) msg := launchtypes.NewMsgRevertLaunch(address, launchID) - _, err := n.cosmos.BroadcastTx(n.account.Name, msg) + _, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return err } diff --git a/ignite/services/network/launch_test.go b/ignite/services/network/launch_test.go index d7acc833e7..9d56ff064c 100644 --- a/ignite/services/network/launch_test.go +++ b/ignite/services/network/launch_test.go @@ -35,7 +35,7 @@ func TestTriggerLaunch(t *testing.T) { }, nil). Once() suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgTriggerLaunch{ + On("BroadcastTx", account, &launchtypes.MsgTriggerLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, RemainingTime: TestMaxRemainingTime, @@ -112,7 +112,7 @@ func TestTriggerLaunch(t *testing.T) { }, nil). Once() suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgTriggerLaunch{ + On("BroadcastTx", account, &launchtypes.MsgTriggerLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, RemainingTime: TestMaxRemainingTime, @@ -140,7 +140,7 @@ func TestTriggerLaunch(t *testing.T) { }, nil). Once() suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgTriggerLaunch{ + On("BroadcastTx", account, &launchtypes.MsgTriggerLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, RemainingTime: TestMaxRemainingTime, @@ -184,7 +184,7 @@ func TestRevertLaunch(t *testing.T) { suite.ChainMock.On("ResetGenesisTime").Return(nil).Once() suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgRevertLaunch{ + On("BroadcastTx", account, &launchtypes.MsgRevertLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, }). @@ -204,7 +204,7 @@ func TestRevertLaunch(t *testing.T) { ) suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgRevertLaunch{ + On("BroadcastTx", account, &launchtypes.MsgRevertLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, }). @@ -232,7 +232,7 @@ func TestRevertLaunch(t *testing.T) { Return(expectedError). Once() suite.CosmosClientMock. - On("BroadcastTx", account.Name, &launchtypes.MsgRevertLaunch{ + On("BroadcastTx", account, &launchtypes.MsgRevertLaunch{ Coordinator: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, }). diff --git a/ignite/services/network/mocks/cosmos_client.go b/ignite/services/network/mocks/cosmos_client.go index c4e7c35a88..71cd776a12 100644 --- a/ignite/services/network/mocks/cosmos_client.go +++ b/ignite/services/network/mocks/cosmos_client.go @@ -23,71 +23,27 @@ type CosmosClient struct { mock.Mock } -// Account provides a mock function with given fields: accountName -func (_m *CosmosClient) Account(accountName string) (cosmosaccount.Account, error) { - ret := _m.Called(accountName) - - var r0 cosmosaccount.Account - if rf, ok := ret.Get(0).(func(string) cosmosaccount.Account); ok { - r0 = rf(accountName) - } else { - r0 = ret.Get(0).(cosmosaccount.Account) - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(accountName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Address provides a mock function with given fields: accountName -func (_m *CosmosClient) Address(accountName string) (types.AccAddress, error) { - ret := _m.Called(accountName) - - var r0 types.AccAddress - if rf, ok := ret.Get(0).(func(string) types.AccAddress); ok { - r0 = rf(accountName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.AccAddress) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(accountName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BroadcastTx provides a mock function with given fields: accountName, msgs -func (_m *CosmosClient) BroadcastTx(accountName string, msgs ...types.Msg) (cosmosclient.Response, error) { +// BroadcastTx provides a mock function with given fields: account, msgs +func (_m *CosmosClient) BroadcastTx(account cosmosaccount.Account, msgs ...types.Msg) (cosmosclient.Response, error) { _va := make([]interface{}, len(msgs)) for _i := range msgs { _va[_i] = msgs[_i] } var _ca []interface{} - _ca = append(_ca, accountName) + _ca = append(_ca, account) _ca = append(_ca, _va...) ret := _m.Called(_ca...) var r0 cosmosclient.Response - if rf, ok := ret.Get(0).(func(string, ...types.Msg) cosmosclient.Response); ok { - r0 = rf(accountName, msgs...) + if rf, ok := ret.Get(0).(func(cosmosaccount.Account, ...types.Msg) cosmosclient.Response); ok { + r0 = rf(account, msgs...) } else { r0 = ret.Get(0).(cosmosclient.Response) } var r1 error - if rf, ok := ret.Get(1).(func(string, ...types.Msg) error); ok { - r1 = rf(accountName, msgs...) + if rf, ok := ret.Get(1).(func(cosmosaccount.Account, ...types.Msg) error); ok { + r1 = rf(account, msgs...) } else { r1 = ret.Error(1) } @@ -95,43 +51,6 @@ func (_m *CosmosClient) BroadcastTx(accountName string, msgs ...types.Msg) (cosm return r0, r1 } -// BroadcastTxWithProvision provides a mock function with given fields: accountName, msgs -func (_m *CosmosClient) BroadcastTxWithProvision(accountName string, msgs ...types.Msg) (uint64, func() (cosmosclient.Response, error), error) { - _va := make([]interface{}, len(msgs)) - for _i := range msgs { - _va[_i] = msgs[_i] - } - var _ca []interface{} - _ca = append(_ca, accountName) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 uint64 - if rf, ok := ret.Get(0).(func(string, ...types.Msg) uint64); ok { - r0 = rf(accountName, msgs...) - } else { - r0 = ret.Get(0).(uint64) - } - - var r1 func() (cosmosclient.Response, error) - if rf, ok := ret.Get(1).(func(string, ...types.Msg) func() (cosmosclient.Response, error)); ok { - r1 = rf(accountName, msgs...) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(func() (cosmosclient.Response, error)) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(string, ...types.Msg) error); ok { - r2 = rf(accountName, msgs...) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // ConsensusInfo provides a mock function with given fields: ctx, height func (_m *CosmosClient) ConsensusInfo(ctx context.Context, height int64) (cosmosclient.ConsensusInfo, error) { ret := _m.Called(ctx, height) diff --git a/ignite/services/network/network.go b/ignite/services/network/network.go index cdb8f5e5e0..e001bdd60f 100644 --- a/ignite/services/network/network.go +++ b/ignite/services/network/network.go @@ -24,11 +24,8 @@ import ( //go:generate mockery --name CosmosClient --case underscore type CosmosClient interface { - Account(accountName string) (cosmosaccount.Account, error) - Address(accountName string) (sdktypes.AccAddress, error) Context() client.Context - BroadcastTx(accountName string, msgs ...sdktypes.Msg) (cosmosclient.Response, error) - BroadcastTxWithProvision(accountName string, msgs ...sdktypes.Msg) (gas uint64, broadcast func() (cosmosclient.Response, error), err error) + BroadcastTx(account cosmosaccount.Account, msgs ...sdktypes.Msg) (cosmosclient.Response, error) Status(ctx context.Context) (*ctypes.ResultStatus, error) ConsensusInfo(ctx context.Context, height int64) (cosmosclient.ConsensusInfo, error) } diff --git a/ignite/services/network/publish.go b/ignite/services/network/publish.go index e31a47097c..f79d2de34d 100644 --- a/ignite/services/network/publish.go +++ b/ignite/services/network/publish.go @@ -137,7 +137,7 @@ func (n Network) Publish(ctx context.Context, c Chain, options ...PublishOption) "", "", ) - if _, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateCoordinator); err != nil { + if _, err := n.cosmos.BroadcastTx(n.account, msgCreateCoordinator); err != nil { return 0, 0, err } } else if err != nil { @@ -185,7 +185,7 @@ func (n Network) Publish(ctx context.Context, c Chain, options ...PublishOption) campaignID, campaigntypes.NewSharesFromCoins(sdk.NewCoins(coins...)), ) - _, err = n.cosmos.BroadcastTx(n.account.Name, msgMintVouchers) + _, err = n.cosmos.BroadcastTx(n.account, msgMintVouchers) if err != nil { return 0, 0, err } @@ -209,7 +209,7 @@ func (n Network) Publish(ctx context.Context, c Chain, options ...PublishOption) campaignID, nil, ) - res, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateChain) + res, err := n.cosmos.BroadcastTx(n.account, msgCreateChain) if err != nil { return 0, 0, err } @@ -244,7 +244,7 @@ func (n Network) sendAccountRequest( ) n.ev.Send(events.New(events.StatusOngoing, "Broadcasting account transactions")) - res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + res, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return err } diff --git a/ignite/services/network/publish_test.go b/ignite/services/network/publish_test.go index 3c7fc5e204..658d41634d 100644 --- a/ignite/services/network/publish_test.go +++ b/ignite/services/network/publish_test.go @@ -59,7 +59,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -121,7 +121,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -193,7 +193,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, campaigntypes.NewMsgMintVouchers( account.Address(networktypes.SPN), testutil.CampaignID, @@ -205,7 +205,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -266,7 +266,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: customGenesisChainID, @@ -317,7 +317,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -371,7 +371,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &campaigntypes.MsgCreateCampaign{ Coordinator: account.Address(networktypes.SPN), CampaignName: testutil.ChainName, @@ -385,7 +385,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &campaigntypes.MsgInitializeMainnet{ Coordinator: account.Address(networktypes.SPN), CampaignID: testutil.CampaignID, @@ -436,7 +436,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &campaigntypes.MsgCreateCampaign{ Coordinator: account.Address(networktypes.SPN), CampaignName: testutil.ChainName, @@ -450,7 +450,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &campaigntypes.MsgInitializeMainnet{ Coordinator: account.Address(networktypes.SPN), CampaignID: testutil.CampaignID, @@ -504,7 +504,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -523,7 +523,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &profiletypes.MsgCreateCoordinator{ Address: account.Address(networktypes.SPN), }, @@ -641,7 +641,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, @@ -692,7 +692,7 @@ func TestPublish(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &launchtypes.MsgCreateChain{ Coordinator: account.Address(networktypes.SPN), GenesisChainID: testutil.ChainID, diff --git a/ignite/services/network/request.go b/ignite/services/network/request.go index 3e746afb80..6a1413b628 100644 --- a/ignite/services/network/request.go +++ b/ignite/services/network/request.go @@ -86,7 +86,7 @@ func (n Network) SubmitRequest(launchID uint64, reviewal ...Reviewal) error { ) } - res, err := n.cosmos.BroadcastTx(n.account.Name, messages...) + res, err := n.cosmos.BroadcastTx(n.account, messages...) if err != nil { return err } diff --git a/ignite/services/network/reward.go b/ignite/services/network/reward.go index 981960a838..3b12da4165 100644 --- a/ignite/services/network/reward.go +++ b/ignite/services/network/reward.go @@ -30,7 +30,7 @@ func (n Network) SetReward(launchID uint64, lastRewardHeight int64, coins sdk.Co lastRewardHeight, coins, ) - res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + res, err := n.cosmos.BroadcastTx(n.account, msg) if err != nil { return err } diff --git a/ignite/services/network/reward_test.go b/ignite/services/network/reward_test.go index d770a75b3b..bf0149bfa7 100644 --- a/ignite/services/network/reward_test.go +++ b/ignite/services/network/reward_test.go @@ -25,7 +25,7 @@ func TestSetReward(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &rewardtypes.MsgSetRewards{ Provider: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, @@ -56,7 +56,7 @@ func TestSetReward(t *testing.T) { suite.CosmosClientMock. On( "BroadcastTx", - account.Name, + account, &rewardtypes.MsgSetRewards{ Provider: account.Address(networktypes.SPN), LaunchID: testutil.LaunchID, diff --git a/integration/app.go b/integration/app.go new file mode 100644 index 0000000000..c1e585d18f --- /dev/null +++ b/integration/app.go @@ -0,0 +1,236 @@ +package envtest + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/pkg/availableport" + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/ignite/pkg/gocmd" + "github.com/ignite/cli/ignite/pkg/xurl" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +const ServeTimeout = time.Minute * 15 + +const defaultConfigFileName = "config.yml" + +type App struct { + path string + configPath string + homePath string + + env Env +} + +type AppOption func(*App) + +func AppConfigPath(path string) AppOption { + return func(o *App) { + o.configPath = path + } +} + +func AppHomePath(path string) AppOption { + return func(o *App) { + o.homePath = path + } +} + +// Scaffold scaffolds an app to a unique appPath and returns it. +func (e Env) Scaffold(name string, flags ...string) App { + root := e.TmpDir() + + e.Exec("scaffold an app", + step.NewSteps(step.New( + step.Exec( + IgniteApp, + append([]string{ + "scaffold", + "chain", + name, + }, flags...)..., + ), + step.Workdir(root), + )), + ) + + var ( + appDirName = path.Base(name) + appSourcePath = filepath.Join(root, appDirName) + appHomePath = e.AppHome(appDirName) + ) + + e.t.Cleanup(func() { os.RemoveAll(appHomePath) }) + + return e.App(appSourcePath, AppHomePath(appHomePath)) +} + +func (e Env) App(path string, options ...AppOption) App { + app := App{ + env: e, + path: path, + } + + for _, apply := range options { + apply(&app) + } + + if app.configPath == "" { + app.configPath = filepath.Join(path, defaultConfigFileName) + } + + return app +} + +func (a App) SourcePath() string { + return a.path +} + +func (a *App) SetConfigPath(path string) { + a.configPath = path +} + +// Binary returns the binary name of the app. Can be executed directly w/o any +// path after app.Serve is called, since it should be in the $PATH. +func (a App) Binary() string { + return path.Base(a.path) + "d" +} + +// Serve serves an application lives under path with options where msg describes the +// execution from the serving action. +// unless calling with Must(), Serve() will not exit test runtime on failure. +func (a App) Serve(msg string, options ...ExecOption) (ok bool) { + serveCommand := []string{ + "chain", + "serve", + "-v", + } + + if a.homePath != "" { + serveCommand = append(serveCommand, "--home", a.homePath) + } + if a.configPath != "" { + serveCommand = append(serveCommand, "--config", a.configPath) + } + + return a.env.Exec(msg, + step.NewSteps(step.New( + step.Exec(IgniteApp, serveCommand...), + step.Workdir(a.path), + )), + options..., + ) +} + +// Simulate runs the simulation test for the app +func (a App) Simulate(numBlocks, blockSize int) { + a.env.Exec("running the simulation tests", + step.NewSteps(step.New( + step.Exec( + IgniteApp, // TODO + "chain", + "simulate", + "--numBlocks", + strconv.Itoa(numBlocks), + "--blockSize", + strconv.Itoa(blockSize), + ), + step.Workdir(a.path), + )), + ) +} + +// EnsureSteady ensures that app living at the path can compile and its tests are passing. +func (a App) EnsureSteady() { + _, statErr := os.Stat(a.configPath) + + require.False(a.env.t, os.IsNotExist(statErr), "config.yml cannot be found") + + a.env.Exec("make sure app is steady", + step.NewSteps(step.New( + step.Exec(gocmd.Name(), "test", "./..."), + step.Workdir(a.path), + )), + ) +} + +// EnableFaucet enables faucet by finding a random port for the app faucet and update config.yml +// with this port and provided coins options. +func (a App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) { + // find a random available port + port, err := availableport.Find(1) + require.NoError(a.env.t, err) + + a.EditConfig(func(conf *chainconfig.Config) { + conf.Faucet.Port = port[0] + conf.Faucet.Coins = coins + conf.Faucet.CoinsMax = coinsMax + }) + + addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0])) + require.NoError(a.env.t, err) + + return addr +} + +// RandomizeServerPorts randomizes server ports for the app at path, updates +// its config.yml and returns new values. +func (a App) RandomizeServerPorts() chainconfig.Host { + // generate random server ports and servers list. + ports, err := availableport.Find(6) + require.NoError(a.env.t, err) + + genAddr := func(port int) string { + return fmt.Sprintf("localhost:%d", port) + } + + servers := chainconfig.Host{ + RPC: genAddr(ports[0]), + P2P: genAddr(ports[1]), + Prof: genAddr(ports[2]), + GRPC: genAddr(ports[3]), + GRPCWeb: genAddr(ports[4]), + API: genAddr(ports[5]), + } + + a.EditConfig(func(conf *chainconfig.Config) { + conf.Host = servers + }) + + return servers +} + +// UseRandomHomeDir sets in the blockchain config files generated temporary directories for home directories +// Returns the random home directory +func (a App) UseRandomHomeDir() (homeDirPath string) { + dir := a.env.TmpDir() + + a.EditConfig(func(conf *chainconfig.Config) { + conf.Init.Home = dir + }) + + return dir +} + +func (a App) EditConfig(apply func(*chainconfig.Config)) { + f, err := os.OpenFile(a.configPath, os.O_RDWR|os.O_CREATE, 0o755) + require.NoError(a.env.t, err) + defer f.Close() + + var conf chainconfig.Config + require.NoError(a.env.t, yaml.NewDecoder(f).Decode(&conf)) + + apply(&conf) + + require.NoError(a.env.t, f.Truncate(0)) + _, err = f.Seek(0, 0) + require.NoError(a.env.t, err) + require.NoError(a.env.t, yaml.NewEncoder(f).Encode(conf)) +} diff --git a/integration/app/cmd_app_test.go b/integration/app/cmd_app_test.go index cfda628644..3316fe604a 100644 --- a/integration/app/cmd_app_test.go +++ b/integration/app/cmd_app_test.go @@ -15,53 +15,53 @@ import ( func TestGenerateAnApp(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) - _, statErr := os.Stat(filepath.Join(path, "x", "blog")) + _, statErr := os.Stat(filepath.Join(app.SourcePath(), "x", "blog")) require.False(t, os.IsNotExist(statErr), "the default module should be scaffolded") - env.EnsureAppIsSteady(path) + app.EnsureSteady() } // TestGenerateAnAppWithName tests scaffolding a new chain using a local name instead of a GitHub URI. func TestGenerateAnAppWithName(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("blog") + env = envtest.New(t) + app = env.Scaffold("blog") ) - _, statErr := os.Stat(filepath.Join(path, "x", "blog")) + _, statErr := os.Stat(filepath.Join(app.SourcePath(), "x", "blog")) require.False(t, os.IsNotExist(statErr), "the default module should be scaffolded") - env.EnsureAppIsSteady(path) + app.EnsureSteady() } func TestGenerateAnAppWithNoDefaultModule(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog", "--no-module") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog", "--no-module") ) - _, statErr := os.Stat(filepath.Join(path, "x", "blog")) + _, statErr := os.Stat(filepath.Join(app.SourcePath(), "x", "blog")) require.True(t, os.IsNotExist(statErr), "the default module should not be scaffolded") - env.EnsureAppIsSteady(path) + app.EnsureSteady() } func TestGenerateAnAppWithNoDefaultModuleAndCreateAModule(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog", "--no-module") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog", "--no-module") ) - defer env.EnsureAppIsSteady(path) + defer app.EnsureSteady() env.Must(env.Exec("should scaffold a new module into a chain that never had modules before", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "first_module"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) } @@ -70,45 +70,45 @@ func TestGenerateAnAppWithWasm(t *testing.T) { t.Skip() var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("add Wasm module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "wasm", "--yes"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should not add Wasm module second time", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "wasm", "--yes"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating an existing module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -116,7 +116,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { env.Must(env.Exec("should prevent creating a module with an invalid name", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example1", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -124,7 +124,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { env.Must(env.Exec("should prevent creating a module with a reserved name", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "tx", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -132,7 +132,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { env.Must(env.Exec("should prevent creating a module with a forbidden prefix", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "ibcfoo", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -140,7 +140,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { env.Must(env.Exec("should prevent creating a module prefixed with an existing module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "examplefoo", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -157,7 +157,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { "account,bank,staking,slashing,example", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -173,7 +173,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { "dup,dup", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -190,10 +190,10 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { "inexistent", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/app/cmd_ibc_test.go b/integration/app/cmd_ibc_test.go index 3a041acc05..de6fa6f794 100644 --- a/integration/app/cmd_ibc_test.go +++ b/integration/app/cmd_ibc_test.go @@ -3,7 +3,6 @@ package app_test import ( - "path/filepath" "testing" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" @@ -12,14 +11,14 @@ import ( func TestCreateModuleWithIBC(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blogibc") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blogibc") ) env.Must(env.Exec("create an IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "foo", "--ibc", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -35,14 +34,14 @@ func TestCreateModuleWithIBC(t *testing.T) { "--path", "./blogibc", ), - step.Workdir(filepath.Dir(path)), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a type in an IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user", "email", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -59,7 +58,7 @@ func TestCreateModuleWithIBC(t *testing.T) { "ordered", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -76,14 +75,14 @@ func TestCreateModuleWithIBC(t *testing.T) { "unordered", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a non IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "non_ibc", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -100,23 +99,23 @@ func TestCreateModuleWithIBC(t *testing.T) { "account,bank,staking,slashing", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } func TestCreateIBCOracle(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/ibcoracle") + env = envtest.New(t) + app = env.Scaffold("github.com/test/ibcoracle") ) env.Must(env.Exec("create an IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "foo", "--ibc", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -133,28 +132,28 @@ func TestCreateIBCOracle(t *testing.T) { "defaultName,isLaunched:bool,minLaunch:uint,maxLaunch:int", "--require-registration", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create the first BandChain oracle integration", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "band", "--yes", "oracleone", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create the second BandChain oracle integration", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "band", "--yes", "oracletwo", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a BandChain oracle with no module specified", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "band", "--yes", "invalidOracle"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -162,7 +161,7 @@ func TestCreateIBCOracle(t *testing.T) { env.Must(env.Exec("should prevent creating a BandChain oracle in a non existent module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "band", "--yes", "invalidOracle", "--module", "nomodule"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -170,31 +169,31 @@ func TestCreateIBCOracle(t *testing.T) { env.Must(env.Exec("create a non-IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "bar", "--params", "name,minLaunch:uint,maxLaunch:int", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a BandChain oracle in a non IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "band", "--yes", "invalidOracle", "--module", "bar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } func TestCreateIBCPacket(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blogibc2") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blogibc2") ) env.Must(env.Exec("create an IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "foo", "--ibc", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -213,14 +212,14 @@ func TestCreateIBCPacket(t *testing.T) { "--ack", "foo:string,bar:int,baz:bool", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a packet with no module specified", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "bar", "text"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -228,7 +227,7 @@ func TestCreateIBCPacket(t *testing.T) { env.Must(env.Exec("should prevent creating a packet in a non existent module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "bar", "text", "--module", "nomodule"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -236,7 +235,7 @@ func TestCreateIBCPacket(t *testing.T) { env.Must(env.Exec("should prevent creating an existing packet", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "bar", "post", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -263,52 +262,52 @@ func TestCreateIBCPacket(t *testing.T) { "victory:bool", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a custom field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "type", "--yes", "custom-type", "customField:uint", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a packet with a custom field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "foo-baz", "customField:CustomType", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a packet with no send message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "nomessage", "foo", "--no-message", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a packet with no field", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "empty", "--module", "foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a non-IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "bar", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a packet in a non IBC module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "packet", "--yes", "foo", "text", "--module", "bar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/app/tx_test.go b/integration/app/tx_test.go index f2c92421a8..c4e4257a73 100644 --- a/integration/app/tx_test.go +++ b/integration/app/tx_test.go @@ -24,8 +24,8 @@ func TestSignTxWithDashedAppName(t *testing.T) { var ( env = envtest.New(t) appname = "dashed-app-name" - path = env.Scaffold(appname) - host = env.RandomizeServerPorts(path, "") + app = env.Scaffold(appname) + host = app.RandomizeServerPorts() ctx, cancel = context.WithCancel(env.Ctx()) ) @@ -36,7 +36,7 @@ func TestSignTxWithDashedAppName(t *testing.T) { env.Exec("scaffold a simple list", step.NewSteps(step.New( - step.Workdir(path), + step.Workdir(app.SourcePath()), step.Exec( envtest.IgniteApp, "scaffold", @@ -60,7 +60,7 @@ func TestSignTxWithDashedAppName(t *testing.T) { steps := step.NewSteps( step.New( step.Exec( - appname+"d", + app.Binary(), "config", "output", "json", ), @@ -75,7 +75,7 @@ func TestSignTxWithDashedAppName(t *testing.T) { return err }), step.Exec( - appname+"d", + app.Binary(), "tx", "dashedappname", "create-item", @@ -102,7 +102,7 @@ func TestSignTxWithDashedAppName(t *testing.T) { isTxBodyRetrieved = env.Exec("sign a tx", steps, envtest.ExecRetry()) }() - env.Must(env.Serve("should serve", path, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve", envtest.ExecCtx(ctx))) if !isTxBodyRetrieved { t.FailNow() @@ -115,8 +115,8 @@ func TestGetTxViaGRPCGateway(t *testing.T) { var ( env = envtest.New(t) appname = randstr.Runes(10) - path = env.Scaffold(fmt.Sprintf("github.com/test/%s", appname)) - host = env.RandomizeServerPorts(path, "") + app = env.Scaffold(fmt.Sprintf("github.com/test/%s", appname)) + host = app.RandomizeServerPorts() ctx, cancel = context.WithCancel(env.Ctx()) ) @@ -143,7 +143,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { steps := step.NewSteps( step.New( step.Exec( - appname+"d", + app.Binary(), "config", "output", "json", ), @@ -153,7 +153,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { ), step.New( step.Exec( - appname+"d", + app.Binary(), "keys", "list", "--keyring-backend", "test", @@ -191,7 +191,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { // endpoint by asserting denom and amount. return cmdrunner.New().Run(ctx, step.New( step.Exec( - appname+"d", + app.Binary(), "tx", "bank", "send", @@ -257,7 +257,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { isTxBodyRetrieved = env.Exec("retrieve account addresses", steps, envtest.ExecRetry()) }() - env.Must(env.Serve("should serve", path, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve", envtest.ExecCtx(ctx))) if !isTxBodyRetrieved { t.FailNow() diff --git a/integration/chain/cache_test.go b/integration/chain/cache_test.go index f815416e70..bfd752fd8a 100644 --- a/integration/chain/cache_test.go +++ b/integration/chain/cache_test.go @@ -18,11 +18,11 @@ import ( func TestCliWithCaching(t *testing.T) { var ( env = envtest.New(t) - path = env.Scaffold("github.com/test/cacheblog") - vueGenerated = filepath.Join(path, "vue/src/store/generated") - openapiGenerated = filepath.Join(path, "docs/static/openapi.yml") - typesDir = filepath.Join(path, "x/cacheblog/types") - servers = env.RandomizeServerPorts(path, "") + app = env.Scaffold("github.com/test/cacheblog") + vueGenerated = filepath.Join(app.SourcePath(), "vue/src/store/generated") + openapiGenerated = filepath.Join(app.SourcePath(), "docs/static/openapi.yml") + typesDir = filepath.Join(app.SourcePath(), "x/cacheblog/types") + servers = app.RandomizeServerPorts() ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error ) @@ -38,7 +38,7 @@ func TestCliWithCaching(t *testing.T) { "myfield2:bool", "--yes", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -52,7 +52,7 @@ func TestCliWithCaching(t *testing.T) { "mytypefield", "--yes", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -64,11 +64,11 @@ func TestCliWithCaching(t *testing.T) { "build", "--proto-all-modules", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() deleteCachedFiles(t, vueGenerated, openapiGenerated, typesDir) @@ -80,11 +80,11 @@ func TestCliWithCaching(t *testing.T) { "build", "--proto-all-modules", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() deleteCachedFiles(t, vueGenerated, openapiGenerated, typesDir) @@ -92,7 +92,7 @@ func TestCliWithCaching(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", path, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } diff --git a/integration/chain/cmd_serve_test.go b/integration/chain/cmd_serve_test.go index d8ccf334a3..27d0a74219 100644 --- a/integration/chain/cmd_serve_test.go +++ b/integration/chain/cmd_serve_test.go @@ -19,14 +19,14 @@ func TestServeStargateWithWasm(t *testing.T) { var ( env = envtest.New(t) - apath = env.Scaffold("github.com/test/sgblog") - servers = env.RandomizeServerPorts(apath, "") + app = env.Scaffold("github.com/test/sgblog") + servers = app.RandomizeServerPorts() ) env.Must(env.Exec("add Wasm module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "wasm", "--yes"), - step.Workdir(apath), + step.Workdir(app.SourcePath()), )), )) @@ -38,7 +38,7 @@ func TestServeStargateWithWasm(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } @@ -46,8 +46,8 @@ func TestServeStargateWithWasm(t *testing.T) { func TestServeStargateWithCustomHome(t *testing.T) { var ( env = envtest.New(t) - apath = env.Scaffold("github.com/test/sgblog2") - servers = env.RandomizeServerPorts(apath, "") + app = env.Scaffold("github.com/test/sgblog2") + servers = app.RandomizeServerPorts() ) var ( @@ -58,7 +58,7 @@ func TestServeStargateWithCustomHome(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "./home", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } @@ -66,13 +66,10 @@ func TestServeStargateWithCustomHome(t *testing.T) { func TestServeStargateWithConfigHome(t *testing.T) { var ( env = envtest.New(t) - apath = env.Scaffold("github.com/test/sgblog3") - servers = env.RandomizeServerPorts(apath, "") + app = env.Scaffold("github.com/test/sgblog3") + servers = app.RandomizeServerPorts() ) - // Set config homes - env.SetRandomHomeConfig(apath, "") - var ( ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error @@ -81,7 +78,7 @@ func TestServeStargateWithConfigHome(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } @@ -90,19 +87,17 @@ func TestServeStargateWithCustomConfigFile(t *testing.T) { tmpDir := t.TempDir() var ( - env = envtest.New(t) - apath = env.Scaffold("github.com/test/sgblog4") + env = envtest.New(t) + app = env.Scaffold("github.com/test/sgblog4") ) // Move config newConfig := "new_config.yml" newConfigPath := filepath.Join(tmpDir, newConfig) - err := xos.Rename(filepath.Join(apath, "config.yml"), newConfigPath) + err := xos.Rename(filepath.Join(app.SourcePath(), "config.yml"), newConfigPath) require.NoError(t, err) + app.SetConfigPath(newConfigPath) - servers := env.RandomizeServerPorts(tmpDir, newConfig) - - // Set config homes - env.SetRandomHomeConfig(tmpDir, newConfig) + servers := app.RandomizeServerPorts() var ( ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) @@ -112,7 +107,7 @@ func TestServeStargateWithCustomConfigFile(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", newConfigPath, envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } @@ -121,12 +116,10 @@ func TestServeStargateWithCustomConfigFile(t *testing.T) { func TestServeStargateWithName(t *testing.T) { var ( env = envtest.New(t) - apath = env.Scaffold("sgblog5") - servers = env.RandomizeServerPorts(apath, "") + app = env.Scaffold("sgblog5") + servers = app.RandomizeServerPorts() ) - env.SetRandomHomeConfig(apath, "") - ctx, cancel := context.WithTimeout(env.Ctx(), envtest.ServeTimeout) var isBackendAliveErr error @@ -137,7 +130,7 @@ func TestServeStargateWithName(t *testing.T) { isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } diff --git a/integration/chain/config_test.go b/integration/chain/config_test.go index 162d65a01f..80b439ae64 100644 --- a/integration/chain/config_test.go +++ b/integration/chain/config_test.go @@ -20,15 +20,15 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { var ( env = envtest.New(t) appname = randstr.Runes(10) - path = env.Scaffold(fmt.Sprintf("github.com/test/%s", appname)) - servers = env.RandomizeServerPorts(path, "") + app = env.Scaffold(fmt.Sprintf("github.com/test/%s", appname)) + servers = app.RandomizeServerPorts() ctx, cancel = context.WithCancel(env.Ctx()) isBackendAliveErr error ) var c chainconfig.Config - cf := confile.New(confile.DefaultYAMLEncodingCreator, filepath.Join(path, "config.yml")) + cf := confile.New(confile.DefaultYAMLEncodingCreator, filepath.Join(app.SourcePath(), "config.yml")) require.NoError(t, cf.Load(&c)) c.Genesis = map[string]interface{}{"chain_id": "cosmos"} @@ -41,7 +41,7 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve", path, "", "", envtest.ExecCtx(ctx))) + env.Must(app.Serve("should serve", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") configs := []struct { @@ -57,7 +57,7 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { for _, c := range configs { var conf map[string]interface{} - cf := confile.New(c.ec, filepath.Join(env.AppdHome(appname), c.relpath)) + cf := confile.New(c.ec, filepath.Join(env.AppHome(appname), c.relpath)) require.NoError(t, cf.Load(&conf)) require.Equal(t, c.expectedVal, conf[c.key]) } diff --git a/integration/cosmosgen/cosmosgen_test.go b/integration/cosmosgen/cosmosgen_test.go index 7d99682433..95c8bbcb17 100644 --- a/integration/cosmosgen/cosmosgen_test.go +++ b/integration/cosmosgen/cosmosgen_test.go @@ -15,8 +15,8 @@ import ( func TestCosmosGen(t *testing.T) { var ( env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") - dirGenerated = filepath.Join(path, "vue/src/store/generated") + app = env.Scaffold("github.com/test/blog") + dirGenerated = filepath.Join(app.SourcePath(), "vue/src/store/generated") ) const ( @@ -33,7 +33,7 @@ func TestCosmosGen(t *testing.T) { "--yes", withMsgModuleName, ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -50,7 +50,7 @@ func TestCosmosGen(t *testing.T) { "--module", withMsgModuleName, ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -63,7 +63,7 @@ func TestCosmosGen(t *testing.T) { "--yes", withoutMsgModuleName, ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -79,7 +79,7 @@ func TestCosmosGen(t *testing.T) { "--module", withoutMsgModuleName, ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -95,7 +95,7 @@ func TestCosmosGen(t *testing.T) { "--module", withoutMsgModuleName, ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -110,7 +110,7 @@ func TestCosmosGen(t *testing.T) { "--yes", "--proto-all-modules", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) diff --git a/integration/env.go b/integration/env.go index c2edc4b544..30a0d17c3f 100644 --- a/integration/env.go +++ b/integration/env.go @@ -1,38 +1,30 @@ package envtest import ( - "bytes" "context" "errors" + "flag" "fmt" - "io" "os" - "path" "path/filepath" "strconv" + "strings" "testing" "time" "github.com/cenkalti/backoff" - "github.com/goccy/go-yaml" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/chainconfig" - "github.com/ignite/cli/ignite/pkg/availableport" - "github.com/ignite/cli/ignite/pkg/cmdrunner" - "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/cosmosfaucet" - "github.com/ignite/cli/ignite/pkg/gocmd" "github.com/ignite/cli/ignite/pkg/httpstatuschecker" "github.com/ignite/cli/ignite/pkg/xexec" "github.com/ignite/cli/ignite/pkg/xurl" ) const ( - ServeTimeout = time.Minute * 15 - IgniteApp = "ignite" - ConfigYML = "config.yml" + IgniteApp = "ignite" + Stargate = "stargate" ) var isCI, _ = strconv.ParseBool(os.Getenv("CI")) @@ -71,185 +63,6 @@ func (e Env) Ctx() context.Context { return e.ctx } -type execOptions struct { - ctx context.Context - shouldErr, shouldRetry bool - stdout, stderr io.Writer -} - -type ExecOption func(*execOptions) - -// ExecShouldError sets the expectations of a command's execution to end with a failure. -func ExecShouldError() ExecOption { - return func(o *execOptions) { - o.shouldErr = true - } -} - -// ExecCtx sets cancelation context for the execution. -func ExecCtx(ctx context.Context) ExecOption { - return func(o *execOptions) { - o.ctx = ctx - } -} - -// ExecStdout captures stdout of an execution. -func ExecStdout(w io.Writer) ExecOption { - return func(o *execOptions) { - o.stdout = w - } -} - -// ExecStderr captures stderr of an execution. -func ExecStderr(w io.Writer) ExecOption { - return func(o *execOptions) { - o.stderr = w - } -} - -// ExecRetry retries command until it is successful before context is canceled. -func ExecRetry() ExecOption { - return func(o *execOptions) { - o.shouldRetry = true - } -} - -// Exec executes a command step with options where msg describes the expectation from the test. -// unless calling with Must(), Exec() will not exit test runtime on failure. -func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool) { - opts := &execOptions{ - ctx: e.ctx, - stdout: io.Discard, - stderr: io.Discard, - } - for _, o := range options { - o(opts) - } - var ( - stdout = &bytes.Buffer{} - stderr = &bytes.Buffer{} - ) - copts := []cmdrunner.Option{ - cmdrunner.DefaultStdout(io.MultiWriter(stdout, opts.stdout)), - cmdrunner.DefaultStderr(io.MultiWriter(stderr, opts.stderr)), - } - if isCI { - copts = append(copts, cmdrunner.EndSignal(os.Kill)) - } - err := cmdrunner. - New(copts...). - Run(opts.ctx, steps...) - if err == context.Canceled { - err = nil - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - if opts.shouldRetry && opts.ctx.Err() == nil { - time.Sleep(time.Second) - return e.Exec(msg, steps, options...) - } - } - if err != nil { - msg = fmt.Sprintf("%s\n\nLogs:\n\n%s\n\nError Logs:\n\n%s\n", - msg, - stdout.String(), - stderr.String()) - } - if opts.shouldErr { - return assert.Error(e.t, err, msg) - } - return assert.NoError(e.t, err, msg) -} - -const ( - Stargate = "stargate" -) - -// Scaffold scaffolds an app to a unique appPath and returns it. -func (e Env) Scaffold(name string, flags ...string) (appPath string) { - root := e.TmpDir() - e.Exec("scaffold an app", - step.NewSteps(step.New( - step.Exec( - IgniteApp, - append([]string{ - "scaffold", - "chain", - name, - }, flags...)..., - ), - step.Workdir(root), - )), - ) - - appDir := path.Base(name) - - // Cleanup the home directory and cache of the app - e.t.Cleanup(func() { - os.RemoveAll(filepath.Join(e.Home(), fmt.Sprintf(".%s", appDir))) - }) - - return filepath.Join(root, appDir) -} - -// Serve serves an application lives under path with options where msg describes the -// execution from the serving action. -// unless calling with Must(), Serve() will not exit test runtime on failure. -func (e Env) Serve(msg, path, home, configPath string, options ...ExecOption) (ok bool) { - serveCommand := []string{ - "chain", - "serve", - "-v", - } - - if home != "" { - serveCommand = append(serveCommand, "--home", home) - } - if configPath != "" { - serveCommand = append(serveCommand, "--config", configPath) - } - - return e.Exec(msg, - step.NewSteps(step.New( - step.Exec(IgniteApp, serveCommand...), - step.Workdir(path), - )), - options..., - ) -} - -// Simulate runs the simulation test for the app -func (e Env) Simulate(appPath string, numBlocks, blockSize int) { - e.Exec("running the simulation tests", - step.NewSteps(step.New( - step.Exec( - IgniteApp, - "chain", - "simulate", - "--numBlocks", - strconv.Itoa(numBlocks), - "--blockSize", - strconv.Itoa(blockSize), - ), - step.Workdir(appPath), - )), - ) -} - -// EnsureAppIsSteady ensures that app living at the path can compile and its tests -// are passing. -func (e Env) EnsureAppIsSteady(appPath string) { - _, statErr := os.Stat(filepath.Join(appPath, ConfigYML)) - require.False(e.t, os.IsNotExist(statErr), "config.yml cannot be found") - - e.Exec("make sure app is steady", - step.NewSteps(step.New( - step.Exec(gocmd.Name(), "test", "./..."), - step.Workdir(appPath), - )), - ) -} - // IsAppServed checks that app is served properly and servers are started to listening // before ctx canceled. func (e Env) IsAppServed(ctx context.Context, host chainconfig.Host) error { @@ -263,8 +76,12 @@ func (e Env) IsAppServed(ctx context.Context, host chainconfig.Host) error { if err == nil && !ok { err = errors.New("app is not online") } + if HasTestVerboseFlag() { + fmt.Printf("IsAppServed at %s: %v\n", addr, err) + } return err } + return backoff.Retry(checkAlive, backoff.WithContext(backoff.NewConstantBackOff(time.Second), ctx)) } @@ -274,108 +91,25 @@ func (e Env) IsFaucetServed(ctx context.Context, faucetClient cosmosfaucet.HTTPC _, err := faucetClient.FaucetInfo(ctx) return err } + return backoff.Retry(checkAlive, backoff.WithContext(backoff.NewConstantBackOff(time.Second), ctx)) } // TmpDir creates a new temporary directory. func (e Env) TmpDir() (path string) { - path, err := os.MkdirTemp("", "integration") - require.NoError(e.t, err, "create a tmp dir") - e.t.Cleanup(func() { os.RemoveAll(path) }) - return path + return e.t.TempDir() } -// RandomizeServerPorts randomizes server ports for the app at path, updates -// its config.yml and returns new values. -func (e Env) RandomizeServerPorts(path string, configFile string) chainconfig.Host { - if configFile == "" { - configFile = ConfigYML - } - - // generate random server ports and servers list. - ports, err := availableport.Find(6) - require.NoError(e.t, err) - - genAddr := func(port int) string { - return fmt.Sprintf("localhost:%d", port) - } - - servers := chainconfig.Host{ - RPC: genAddr(ports[0]), - P2P: genAddr(ports[1]), - Prof: genAddr(ports[2]), - GRPC: genAddr(ports[3]), - GRPCWeb: genAddr(ports[4]), - API: genAddr(ports[5]), - } - - // update config.yml with the generated servers list. - configyml, err := os.OpenFile(filepath.Join(path, configFile), os.O_RDWR|os.O_CREATE, 0o755) - require.NoError(e.t, err) - defer configyml.Close() - - var conf chainconfig.Config - require.NoError(e.t, yaml.NewDecoder(configyml).Decode(&conf)) - - conf.Host = servers - require.NoError(e.t, configyml.Truncate(0)) - _, err = configyml.Seek(0, 0) - require.NoError(e.t, err) - require.NoError(e.t, yaml.NewEncoder(configyml).Encode(conf)) - - return servers -} - -// ConfigureFaucet finds a random port for the app faucet and updates config.yml with this port and provided coins options -func (e Env) ConfigureFaucet(path string, configFile string, coins, coinsMax []string) string { - if configFile == "" { - configFile = ConfigYML - } - - // find a random available port - port, err := availableport.Find(1) - require.NoError(e.t, err) - - configyml, err := os.OpenFile(filepath.Join(path, configFile), os.O_RDWR|os.O_CREATE, 0o755) - require.NoError(e.t, err) - defer configyml.Close() - - var conf chainconfig.Config - require.NoError(e.t, yaml.NewDecoder(configyml).Decode(&conf)) - - conf.Faucet.Port = port[0] - conf.Faucet.Coins = coins - conf.Faucet.CoinsMax = coinsMax - require.NoError(e.t, configyml.Truncate(0)) - _, err = configyml.Seek(0, 0) - require.NoError(e.t, err) - require.NoError(e.t, yaml.NewEncoder(configyml).Encode(conf)) - - addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0])) +// Home returns user's home dir. +func (e Env) Home() string { + home, err := os.UserHomeDir() require.NoError(e.t, err) - - return addr + return home } -// SetRandomHomeConfig sets in the blockchain config files generated temporary directories for home directories -func (e Env) SetRandomHomeConfig(path string, configFile string) { - if configFile == "" { - configFile = ConfigYML - } - - // update config.yml with the generated temporary directories - configyml, err := os.OpenFile(filepath.Join(path, configFile), os.O_RDWR|os.O_CREATE, 0o755) - require.NoError(e.t, err) - defer configyml.Close() - - var conf chainconfig.Config - require.NoError(e.t, yaml.NewDecoder(configyml).Decode(&conf)) - - conf.Init.Home = e.TmpDir() - require.NoError(e.t, configyml.Truncate(0)) - _, err = configyml.Seek(0, 0) - require.NoError(e.t, err) - require.NoError(e.t, yaml.NewEncoder(configyml).Encode(conf)) +// AppHome returns app's root home/data dir path. +func (e Env) AppHome(name string) string { + return filepath.Join(e.Home(), fmt.Sprintf(".%s", name)) } // Must fails the immediately if not ok. @@ -386,14 +120,18 @@ func (e Env) Must(ok bool) { } } -// Home returns user's home dir. -func (e Env) Home() string { - home, err := os.UserHomeDir() - require.NoError(e.t, err) - return home +func (e Env) HasFailed() bool { + return e.t.Failed() } -// AppdHome returns appd's home dir. -func (e Env) AppdHome(name string) string { - return filepath.Join(e.Home(), fmt.Sprintf(".%s", name)) +func (e Env) RequireExpectations() { + e.Must(e.HasFailed()) +} + +func Contains(s, partial string) bool { + return strings.Contains(s, strings.TrimSpace(partial)) +} + +func HasTestVerboseFlag() bool { + return flag.Lookup("test.v").Value.String() == "true" } diff --git a/integration/exec.go b/integration/exec.go new file mode 100644 index 0000000000..cdbc1951fb --- /dev/null +++ b/integration/exec.go @@ -0,0 +1,109 @@ +package envtest + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "time" + + "github.com/ignite/cli/ignite/pkg/cmdrunner" + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/stretchr/testify/assert" +) + +type execOptions struct { + ctx context.Context + shouldErr, shouldRetry bool + stdout, stderr io.Writer +} + +type ExecOption func(*execOptions) + +// ExecShouldError sets the expectations of a command's execution to end with a failure. +func ExecShouldError() ExecOption { + return func(o *execOptions) { + o.shouldErr = true + } +} + +// ExecCtx sets cancelation context for the execution. +func ExecCtx(ctx context.Context) ExecOption { + return func(o *execOptions) { + o.ctx = ctx + } +} + +// ExecStdout captures stdout of an execution. +func ExecStdout(w io.Writer) ExecOption { + return func(o *execOptions) { + o.stdout = w + } +} + +// ExecStderr captures stderr of an execution. +func ExecStderr(w io.Writer) ExecOption { + return func(o *execOptions) { + o.stderr = w + } +} + +// ExecRetry retries command until it is successful before context is canceled. +func ExecRetry() ExecOption { + return func(o *execOptions) { + o.shouldRetry = true + } +} + +// Exec executes a command step with options where msg describes the expectation from the test. +// unless calling with Must(), Exec() will not exit test runtime on failure. +func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool) { + opts := &execOptions{ + ctx: e.ctx, + stdout: io.Discard, + stderr: io.Discard, + } + for _, o := range options { + o(opts) + } + var ( + stdout = &bytes.Buffer{} + stderr = &bytes.Buffer{} + ) + copts := []cmdrunner.Option{ + cmdrunner.DefaultStdout(io.MultiWriter(stdout, opts.stdout)), + cmdrunner.DefaultStderr(io.MultiWriter(stderr, opts.stderr)), + } + if HasTestVerboseFlag() { + fmt.Printf("Executing %d step(s) for %q\n", len(steps), msg) + copts = append(copts, cmdrunner.EnableDebug()) + } + if isCI { + copts = append(copts, cmdrunner.EndSignal(os.Kill)) + } + err := cmdrunner. + New(copts...). + Run(opts.ctx, steps...) + if err == context.Canceled { + err = nil + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + if opts.shouldRetry && opts.ctx.Err() == nil { + time.Sleep(time.Second) + return e.Exec(msg, steps, options...) + } + } + + if err != nil { + msg = fmt.Sprintf("%s\n\nLogs:\n\n%s\n\nError Logs:\n\n%s\n", + msg, + stdout.String(), + stderr.String()) + } + if opts.shouldErr { + return assert.Error(e.t, err, msg) + } + return assert.NoError(e.t, err, msg) +} diff --git a/integration/faucet/faucet_test.go b/integration/faucet/faucet_test.go index c6e9633763..b3e7fc029f 100644 --- a/integration/faucet/faucet_test.go +++ b/integration/faucet/faucet_test.go @@ -32,9 +32,9 @@ var ( func TestRequestCoinsFromFaucet(t *testing.T) { var ( env = envtest.New(t) - apath = env.Scaffold("github.com/test/faucet") - servers = env.RandomizeServerPorts(apath, "") - faucetURL = env.ConfigureFaucet(apath, "", defaultCoins, maxCoins) + app = env.Scaffold("github.com/test/faucet") + servers = app.RandomizeServerPorts() + faucetURL = app.EnableFaucet(defaultCoins, maxCoins) ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) faucetClient = cosmosfaucet.NewClient(faucetURL) ) @@ -46,7 +46,7 @@ func TestRequestCoinsFromFaucet(t *testing.T) { // serve the app go func() { - env.Serve("should serve app", apath, "", "", envtest.ExecCtx(ctx)) + app.Serve("should serve app", envtest.ExecCtx(ctx)) }() // wait servers to be online diff --git a/integration/list/cmd_list_test.go b/integration/list/cmd_list_test.go index 3dba5eb23e..a77bf7a794 100644 --- a/integration/list/cmd_list_test.go +++ b/integration/list/cmd_list_test.go @@ -3,7 +3,6 @@ package list_test import ( - "path/filepath" "testing" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" @@ -12,21 +11,21 @@ import ( func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a list", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user", "email"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -43,7 +42,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { "--module", "example", ), - step.Workdir(filepath.Dir(path)), + step.Workdir(app.SourcePath()), )), )) @@ -68,7 +67,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { "textCoinsAlias:coins", "--no-simulation", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -83,7 +82,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { "--module", "example", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -98,14 +97,14 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { "--module", "example", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a list with duplicated fields", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "company", "name", "name"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -113,7 +112,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { env.Must(env.Exec("should prevent creating a list with unrecognized field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "employee", "level:itn"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -121,7 +120,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { env.Must(env.Exec("should prevent creating an existing list", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user", "email"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -129,7 +128,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { env.Must(env.Exec("should prevent creating a list whose name is a reserved word", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "map", "size:int"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -137,7 +136,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { env.Must(env.Exec("should prevent creating a list containing a field with a reserved word", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "document", "type:int"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -145,17 +144,17 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { env.Must(env.Exec("create a list with no interaction message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "nomessage", "email", "--no-message"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a list in a non existent module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user", "email", "--module", "idontexist"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/map/cmd_map_test.go b/integration/map/cmd_map_test.go index aa7e6e226b..e549ed92c6 100644 --- a/integration/map/cmd_map_test.go +++ b/integration/map/cmd_map_test.go @@ -12,35 +12,35 @@ import ( func TestCreateMapWithStargate(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a map", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "user", "user-id", "email"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a map with custom path", step.NewSteps(step.New( - step.Exec(envtest.IgniteApp, "s", "map", "--yes", "appPath", "email", "--path", filepath.Join(path, "app")), - step.Workdir(filepath.Dir(path)), + step.Exec(envtest.IgniteApp, "s", "map", "--yes", "appPath", "email", "--path", filepath.Join(app.SourcePath(), "app")), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a map with no message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "nomessage", "email", "--no-message"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -57,14 +57,14 @@ func TestCreateMapWithStargate(t *testing.T) { "example", "--no-simulation", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a map with a typename that already exist", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "user", "email", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -72,14 +72,14 @@ func TestCreateMapWithStargate(t *testing.T) { env.Must(env.Exec("create a map in a custom module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "mapUser", "email", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a map with a custom field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "mapDetail", "user:MapUser", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -105,7 +105,7 @@ func TestCreateMapWithStargate(t *testing.T) { "--module", "example", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -124,7 +124,7 @@ func TestCreateMapWithStargate(t *testing.T) { "--module", "example", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -142,7 +142,7 @@ func TestCreateMapWithStargate(t *testing.T) { "--module", "example", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -151,14 +151,14 @@ func TestCreateMapWithStargate(t *testing.T) { step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "message", "--yes", "create-scavenge", "description"), step.Exec(envtest.IgniteApp, "s", "map", "--yes", "scavenge", "description", "--no-message"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating a map with duplicated indexes", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "map_with_duplicated_index", "email", "--index", "foo,foo"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -166,10 +166,10 @@ func TestCreateMapWithStargate(t *testing.T) { env.Must(env.Exec("should prevent creating a map with an index present in fields", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "map", "--yes", "map_with_invalid_index", "email", "--index", "email"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/node/cmd_query_bank_test.go b/integration/node/cmd_query_bank_test.go new file mode 100644 index 0000000000..43159a1fbf --- /dev/null +++ b/integration/node/cmd_query_bank_test.go @@ -0,0 +1,280 @@ +package node_test + +import ( + "bytes" + "context" + "path/filepath" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/pkg/cliui/entrywriter" + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/ignite/pkg/cosmosaccount" + "github.com/ignite/cli/ignite/pkg/cosmosclient" + "github.com/ignite/cli/ignite/pkg/randstr" + "github.com/ignite/cli/ignite/pkg/xurl" + envtest "github.com/ignite/cli/integration" +) + +const ( + keyringTestDirName = "keyring-test" + testPrefix = "testpref" +) + +func assertBankBalanceOutput(t *testing.T, output string, balances string) { + var table [][]string + coins, err := sdktypes.ParseCoinsNormalized(balances) + require.NoError(t, err, "wrong balances %s", balances) + for _, c := range coins { + table = append(table, []string{c.Amount.String(), c.Denom}) + } + var expectedBalances strings.Builder + entrywriter.MustWrite(&expectedBalances, []string{"Amount", "Denom"}, table...) + assert.Contains(t, output, expectedBalances.String()) +} + +func TestNodeQueryBankBalances(t *testing.T) { + var ( + appname = randstr.Runes(10) + alice = "alice" + + env = envtest.New(t) + app = env.Scaffold(appname, "--address-prefix", testPrefix) + home = env.AppHome(appname) + servers = app.RandomizeServerPorts() + + accKeyringDir = t.TempDir() + ) + + node, err := xurl.HTTP(servers.RPC) + require.NoError(t, err) + + ca, err := cosmosaccount.New( + cosmosaccount.WithHome(filepath.Join(home, keyringTestDirName)), + cosmosaccount.WithKeyringBackend(cosmosaccount.KeyringTest), + ) + require.NoError(t, err) + + aliceAccount, aliceMnemonic, err := ca.Create(alice) + require.NoError(t, err) + + app.EditConfig(func(conf *chainconfig.Config) { + conf.Accounts = []chainconfig.Account{ + { + Name: alice, + Mnemonic: aliceMnemonic, + Coins: []string{"5600atoken", "1200btoken", "100000000stake"}, + }, + } + conf.Faucet = chainconfig.Faucet{} + conf.Init.KeyringBackend = keyring.BackendTest + }) + + env.Must(env.Exec("import alice", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "account", + "import", + alice, + "--keyring-dir", accKeyringDir, + "--non-interactive", + "--secret", aliceMnemonic, + ), + )), + )) + + var ( + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) + isBackendAliveErr error + ) + + // do not fail the test in a goroutine, it has to be done in the main. + go func() { + defer cancel() + + if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + return + } + + client, err := cosmosclient.New(context.Background(), + cosmosclient.WithAddressPrefix(testPrefix), + cosmosclient.WithNodeAddress(node), + ) + require.NoError(t, err) + require.NoError(t, client.WaitForNextBlock()) + + b := &bytes.Buffer{} + + env.Exec("query bank balances by account name", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "alice", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecStdout(b), + ) + + if env.HasFailed() { + return + } + + assertBankBalanceOutput(t, b.String(), "5600atoken,1200btoken") + + b.Reset() + env.Exec("query bank balances by address", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + aliceAccount.Address(testPrefix), + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecStdout(b), + ) + + if env.HasFailed() { + return + } + + assertBankBalanceOutput(t, b.String(), "5600atoken,1200btoken") + + b.Reset() + env.Exec("query bank balances with pagination -page 1", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "alice", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--limit", "1", + "--page", "1", + ), + )), + envtest.ExecStdout(b), + ) + + if env.HasFailed() { + return + } + + assertBankBalanceOutput(t, b.String(), "5600atoken") + assert.NotContains(t, b.String(), "btoken") + + b.Reset() + env.Exec("query bank balances with pagination -page 2", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "alice", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--limit", "1", + "--page", "2", + ), + )), + envtest.ExecStdout(b), + ) + + if env.HasFailed() { + return + } + + assertBankBalanceOutput(t, b.String(), "1200btoken") + assert.NotContains(t, b.String(), "atoken") + + b.Reset() + env.Exec("query bank balances fail with non-existent account name", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "nonexistentaccount", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecShouldError(), + ) + + if env.HasFailed() { + return + } + + env.Exec("query bank balances fail with non-existent address", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + testPrefix+"1gspvt8qsk8cryrsxnqt452cjczjm5ejdgla24e", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecShouldError(), + ) + + if env.HasFailed() { + return + } + + env.Exec("query bank balances should fail with a wrong prefix", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "alice", + "--node", node, + "--keyring-dir", accKeyringDir, + // the default prefix will fail this test, which is on purpose. + ), + )), + envtest.ExecShouldError(), + ) + }() + + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) + + require.NoError(t, isBackendAliveErr, "app cannot get online in time") +} diff --git a/integration/node/cmd_query_tx_test.go b/integration/node/cmd_query_tx_test.go new file mode 100644 index 0000000000..7506d9a915 --- /dev/null +++ b/integration/node/cmd_query_tx_test.go @@ -0,0 +1,97 @@ +package node_test + +import ( + "bytes" + "context" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/ignite/pkg/cosmosclient" + "github.com/ignite/cli/ignite/pkg/randstr" + "github.com/ignite/cli/ignite/pkg/xurl" + envtest "github.com/ignite/cli/integration" +) + +func TestNodeQueryTx(t *testing.T) { + var ( + appname = randstr.Runes(10) + // alice = "alice" + // bob = "bob" + + env = envtest.New(t) + app = env.Scaffold(appname) + home = env.AppHome(appname) + servers = app.RandomizeServerPorts() + + // accKeyringDir = t.TempDir() + ) + + node, err := xurl.HTTP(servers.RPC) + require.NoError(t, err) + + var ( + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) + isBackendAliveErr error + ) + + go func() { + defer cancel() + + if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + return + } + client, err := cosmosclient.New(context.Background(), + cosmosclient.WithAddressPrefix(testPrefix), + cosmosclient.WithNodeAddress(node), + ) + require.NoError(t, err) + require.NoError(t, client.WaitForNextBlock()) + + b := &bytes.Buffer{} + env.Exec("send 100token from alice to bob", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + "bob", + "100token", + "--node", node, + "--keyring-dir", home, + "--broadcast-mode", "sync", + ), + step.Stdout(b), + )), + ) + require.False(t, env.HasFailed(), b.String()) + + // Parse tx hash from output + res := regexp.MustCompile(`\(hash = (\w+)\)`).FindAllStringSubmatch(b.String(), -1) + require.Len(t, res[0], 2, "can't extract hash from command output") + hash := res[0][1] + require.NoError(t, client.WaitForNextBlock()) + + env.Must(env.Exec("query tx", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "tx", + hash, + "--node", node, + ), + )), + )) + }() + + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) + + require.NoError(t, isBackendAliveErr, "app cannot get online in time") +} diff --git a/integration/node/cmd_tx_bank_send_test.go b/integration/node/cmd_tx_bank_send_test.go new file mode 100644 index 0000000000..766c6f28c1 --- /dev/null +++ b/integration/node/cmd_tx_bank_send_test.go @@ -0,0 +1,322 @@ +package node_test + +import ( + "bytes" + "context" + "fmt" + "path/filepath" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/ignite/pkg/cosmosaccount" + "github.com/ignite/cli/ignite/pkg/cosmosclient" + "github.com/ignite/cli/ignite/pkg/randstr" + "github.com/ignite/cli/ignite/pkg/xurl" + envtest "github.com/ignite/cli/integration" +) + +func TestNodeTxBankSend(t *testing.T) { + var ( + appname = randstr.Runes(10) + alice = "alice" + bob = "bob" + + env = envtest.New(t) + app = env.Scaffold(appname, "--address-prefix", testPrefix) + home = env.AppHome(appname) + servers = app.RandomizeServerPorts() + + accKeyringDir = t.TempDir() + ) + + node, err := xurl.HTTP(servers.RPC) + require.NoError(t, err) + + ca, err := cosmosaccount.New( + cosmosaccount.WithHome(filepath.Join(home, keyringTestDirName)), + cosmosaccount.WithKeyringBackend(cosmosaccount.KeyringTest), + ) + require.NoError(t, err) + + aliceAccount, aliceMnemonic, err := ca.Create(alice) + require.NoError(t, err) + + bobAccount, bobMnemonic, err := ca.Create(bob) + require.NoError(t, err) + + app.EditConfig(func(conf *chainconfig.Config) { + conf.Accounts = []chainconfig.Account{ + { + Name: alice, + Mnemonic: aliceMnemonic, + Coins: []string{"2000token", "100000000stake"}, + }, + { + Name: bob, + Mnemonic: bobMnemonic, + Coins: []string{"10000token", "100000000stake"}, + }, + } + conf.Faucet = chainconfig.Faucet{} + conf.Init.KeyringBackend = keyring.BackendTest + }) + env.Must(env.Exec("import alice", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "account", + "import", + "alice", + "--keyring-dir", accKeyringDir, + "--non-interactive", + "--secret", aliceMnemonic, + ), + )), + )) + + env.Must(env.Exec("import bob", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "account", + "import", + "bob", + "--keyring-dir", accKeyringDir, + "--non-interactive", + "--secret", bobMnemonic, + ), + )), + )) + + var ( + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) + isBackendAliveErr error + ) + + go func() { + defer cancel() + + if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + return + } + client, err := cosmosclient.New(context.Background(), + cosmosclient.WithAddressPrefix(testPrefix), + cosmosclient.WithNodeAddress(node), + ) + require.NoError(t, err) + require.NoError(t, client.WaitForNextBlock()) + + env.Exec("send 100token from alice to bob", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + "bob", + "100token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + ) + + if env.HasFailed() { + return + } + + env.Exec("send 2stake from bob to alice using addresses", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + bobAccount.Address(testPrefix), + aliceAccount.Address(testPrefix), + "2stake", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + ) + + if env.HasFailed() { + return + } + + env.Exec("send 5token from alice to bob using a combination of address and account", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + bobAccount.Address(testPrefix), + "5token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + ) + + if env.HasFailed() { + return + } + + b := &bytes.Buffer{} + env.Exec("query bank balances for alice", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "alice", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecStdout(b), + ) + + assertBankBalanceOutput(t, b.String(), "2stake,1895token") + + if env.HasFailed() { + return + } + + b.Reset() + env.Exec("query bank balances for bob", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "query", + "bank", + "balances", + "bob", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + ), + )), + envtest.ExecStdout(b), + ) + + assertBankBalanceOutput(t, b.String(), "99999998stake,10105token") + + // check generated tx + b.Reset() + env.Exec("generate unsigned tx", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + "bob", + "5token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--generate-only", + ), + )), + envtest.ExecStdout(b), + ) + + require.Contains(t, b.String(), + fmt.Sprintf(`"body":{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgSend","from_address":"%s","to_address":"%s","amount":[{"denom":"token","amount":"5"}]}]`, + aliceAccount.Address(testPrefix), bobAccount.Address(testPrefix)), + ) + require.Contains(t, b.String(), `"signatures":[]`) + + // test with gas + env.Exec("send 100token from bob to alice with gas flags", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "bob", + "alice", + "100token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--gas", "200000", + "--gas-prices", "1stake", + ), + )), + ) + + // not enough minerals + env.Exec("send 100token from alice to bob with too little gas", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + "bob", + "100token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--gas", "2", + "--gas-prices", "1stake", + ), + )), + envtest.ExecShouldError(), + ) + + b.Reset() + env.Exec("generate bank send tx with gas flags", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "node", + "tx", + "bank", + "send", + "alice", + "bob", + "100token", + "--node", node, + "--keyring-dir", accKeyringDir, + "--address-prefix", testPrefix, + "--gas", "2000034", + "--gas-prices", "0.089stake", + "--generate-only", + ), + )), + envtest.ExecStdout(b), + ) + require.Contains(t, b.String(), `"fee":{"amount":[{"denom":"stake","amount":"178004"}],"gas_limit":"2000034"`) + }() + + env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) + + require.NoError(t, isBackendAliveErr, "app cannot get online in time") +} diff --git a/integration/other_components/cmd_message_test.go b/integration/other_components/cmd_message_test.go index 9106485764..bba8ae0615 100644 --- a/integration/other_components/cmd_message_test.go +++ b/integration/other_components/cmd_message_test.go @@ -3,7 +3,6 @@ package other_components_test import ( - "path/filepath" "testing" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" @@ -12,8 +11,8 @@ import ( func TestGenerateAnAppWithMessage(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a message", @@ -30,7 +29,7 @@ func TestGenerateAnAppWithMessage(t *testing.T) { "-r", "foo,bar:int,foobar:bool", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -51,14 +50,14 @@ func TestGenerateAnAppWithMessage(t *testing.T) { "blog", "--no-simulation", ), - step.Workdir(filepath.Dir(path)), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating an existing message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "message", "--yes", "do-foo", "bar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -66,7 +65,7 @@ func TestGenerateAnAppWithMessage(t *testing.T) { env.Must(env.Exec("create a message with a custom signer name", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "message", "--yes", "do-bar", "bar", "--signer", "bar-doer"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -90,21 +89,21 @@ func TestGenerateAnAppWithMessage(t *testing.T) { "textCoins:array.coin", "textCoinsAlias:coins", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a message with the custom field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "message", "--yes", "foo-baz", "customField:CustomType"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "foo", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -125,9 +124,9 @@ func TestGenerateAnAppWithMessage(t *testing.T) { "--response", "foo,bar:int,foobar:bool", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/other_components/cmd_query_test.go b/integration/other_components/cmd_query_test.go index b29a1b2dae..94ec3dc46d 100644 --- a/integration/other_components/cmd_query_test.go +++ b/integration/other_components/cmd_query_test.go @@ -3,7 +3,6 @@ package other_components_test import ( - "path/filepath" "testing" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" @@ -12,8 +11,8 @@ import ( func TestGenerateAnAppWithQuery(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a query", @@ -30,7 +29,7 @@ func TestGenerateAnAppWithQuery(t *testing.T) { "-r", "foo,bar:int,foobar:bool", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -50,7 +49,7 @@ func TestGenerateAnAppWithQuery(t *testing.T) { "--path", "./blog", ), - step.Workdir(filepath.Dir(path)), + step.Workdir(app.SourcePath()), )), )) @@ -69,7 +68,7 @@ func TestGenerateAnAppWithQuery(t *testing.T) { "foo,bar:int,foobar:bool", "--paginated", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -93,21 +92,21 @@ func TestGenerateAnAppWithQuery(t *testing.T) { "textCoins:array.coin", "textCoinsAlias:coins", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a query with the custom field type as a response", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "query", "--yes", "foobaz", "-r", "bar:CustomType"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent using custom type in request params", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "query", "--yes", "bur", "bar:CustomType"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -115,14 +114,14 @@ func TestGenerateAnAppWithQuery(t *testing.T) { env.Must(env.Exec("create an empty query", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "query", "--yes", "foobar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating an existing query", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "query", "--yes", "foo", "bar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -130,7 +129,7 @@ func TestGenerateAnAppWithQuery(t *testing.T) { env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "foo", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -150,9 +149,9 @@ func TestGenerateAnAppWithQuery(t *testing.T) { "--response", "foo,bar:int,foobar:bool", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() } diff --git a/integration/simulation/simapp_test.go b/integration/simulation/simapp_test.go index bb12a26d73..1718cbe59f 100644 --- a/integration/simulation/simapp_test.go +++ b/integration/simulation/simapp_test.go @@ -11,42 +11,42 @@ import ( func TestGenerateAnAppAndSimulate(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create a list", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "foo", "foobar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create an singleton type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "single", "--yes", "baz", "foobar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create an singleton type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "noSimapp", "foobar", "--no-simulation"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "message", "--yes", "msgFoo", "foobar"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("scaffold a new module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "new_module"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) @@ -62,9 +62,9 @@ func TestGenerateAnAppAndSimulate(t *testing.T) { "--module", "new_module", ), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.Simulate(path, 100, 50) + app.Simulate(100, 50) } diff --git a/integration/single/cmd_singleton_test.go b/integration/single/cmd_singleton_test.go index bad28e6782..e412083e4e 100644 --- a/integration/single/cmd_singleton_test.go +++ b/integration/single/cmd_singleton_test.go @@ -3,7 +3,6 @@ package single_test import ( - "path/filepath" "testing" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" @@ -12,56 +11,56 @@ import ( func TestCreateSingletonWithStargate(t *testing.T) { var ( - env = envtest.New(t) - path = env.Scaffold("github.com/test/blog") + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") ) env.Must(env.Exec("create an singleton type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "single", "--yes", "user", "email"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create an singleton type with custom path", step.NewSteps(step.New( - step.Exec(envtest.IgniteApp, "s", "single", "--yes", "appPath", "email", "--path", path), - step.Workdir(filepath.Dir(path)), + step.Exec(envtest.IgniteApp, "s", "single", "--yes", "appPath", "email", "--path", app.SourcePath()), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create an singleton type with no message", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "single", "--yes", "no-message", "email", "--no-message"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create a module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "module", "--yes", "example", "--require-registration"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create another type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user", "email", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("create another type with a custom field type", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "list", "--yes", "user-detail", "user:User", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) env.Must(env.Exec("should prevent creating an singleton type with a typename that already exist", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "single", "--yes", "user", "email", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), envtest.ExecShouldError(), )) @@ -69,9 +68,9 @@ func TestCreateSingletonWithStargate(t *testing.T) { env.Must(env.Exec("create an singleton type in a custom module", step.NewSteps(step.New( step.Exec(envtest.IgniteApp, "s", "single", "--yes", "singleuser", "email", "--module", "example"), - step.Workdir(path), + step.Workdir(app.SourcePath()), )), )) - env.EnsureAppIsSteady(path) + app.EnsureSteady() }