From 781e90a5e43aa83d3dbe9179eba107cd140711c9 Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Mon, 22 Aug 2022 12:23:01 +0200 Subject: [PATCH] feat(cmd): introduce node command (#2669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add node command with bank query and send Bounty address: cosmos1zra3wz596n0qc898myceka3q23x9rk8fuw65c7 * Code review cleanup * Code review fix * fix merge * refactor * Merge branch 'develop' into issue-2489-node-banks-cmds Fixed a bunch of conflicts with recent renaming * wip: fix integration tests due to recent refac * test: finish apply refac in tests * Fix integration test on node command * various improvments in node integration test * test: read test.v to add more trace on step execution That helps when some commands are not running properly. * fix: spinner hides os keyring prompt * Ensure bank send can take account name or address Also simplify the logic by removing the read of the --from flag, which is no longer used in this command. BroadcastTx now takes a cosmosaccount.Account instead of just an account name. Since ensure the account comes from the keyring, and avoid the BroadcastTx impl to re-check that (because the keyring is always checked before the call to BroadcastTx). * fix after rebase * fix: bank balance can take an address absent of the keyring * fix tx broadcast error message * add fees flag to node tx bank send cmd fees are required, at least in cosmos-hub to send funds to an other account. W/o fees the transaction returns this message: error code: '13' msg: 'insufficient fees; got: required: 8uatom: insufficient fee' Also increase the additional gas amount added to the simulated gas, because I got some insufficient gas error with the previous value. * Change tx BroadcastMode to BroadcastSync Previous mode BroadcastBlock alweus triggered a timeout error, even if the tx was finally accepted in a block. RPC error -32603 - Internal error: timed out waiting for tx to be included in a block * fix linter * fix other lint * improve lookupAddress error report * fix typo * wip * simplify * add assertBankBalanceOutput * wip find a way to wait for block height * test: replace time.Sleep with app.WaitNBlocks * gofumpt * fix integration test * BroadcastBlock is deprecated * simplify node query * feat: move WaitForBlock methods in cosmosclient * fix: revert usage of BroadcastSync mode * docs: adapt blog tutorial to cosmosclient changes * feat: add --broadcasd-mode flag to node tx command The default value is still block but the flag can change to sync in order to avoid timeout when the tx is broadcasted to busy nodes. Also add a `node query tx` so the user can check when his tx is included in a block. * comments * style: consolidate arg names * fix: add port to default node * merge const decls * fix merge error * fix after merge * fix after merge Co-authored-by: Gjermund Bjaanes Co-authored-by: İlker G. Öztürk Co-authored-by: Alex Johnson --- .../guide/03-blog/02-connect-blockchain.md | 2 +- go.mod | 2 +- ignite/cmd/cmd.go | 1 + ignite/cmd/network.go | 1 + ignite/cmd/network_campaign_publish.go | 1 + ignite/cmd/network_campaign_update.go | 1 + ignite/cmd/network_chain_init.go | 1 + ignite/cmd/network_chain_join.go | 1 + ignite/cmd/network_chain_launch.go | 1 + ignite/cmd/network_chain_prepare.go | 1 + ignite/cmd/network_chain_publish.go | 1 + ignite/cmd/network_chain_revert_launch.go | 1 + ignite/cmd/network_request_approve.go | 1 + ignite/cmd/network_request_reject.go | 1 + ignite/cmd/network_request_verify.go | 1 + ignite/cmd/network_reward_set.go | 1 + ignite/cmd/node.go | 67 ++++ ignite/cmd/node_query.go | 72 ++++ ignite/cmd/node_query_bank.go | 14 + ignite/cmd/node_query_bank_balances.go | 61 ++++ ignite/cmd/node_query_tx.go | 41 +++ ignite/cmd/node_tx.go | 76 +++++ ignite/cmd/node_tx_bank.go | 14 + ignite/cmd/node_tx_bank_send.go | 83 +++++ ignite/cmd/relayer_configure.go | 2 + ignite/cmd/relayer_connect.go | 2 + ignite/pkg/chaincmd/chaincmd.go | 5 +- ignite/pkg/cmdrunner/cmdrunner.go | 14 +- ignite/pkg/cosmosaccount/cosmosaccount.go | 23 +- .../pkg/cosmosaccount/cosmosaccount_test.go | 71 ++++ ignite/pkg/cosmosclient/bank.go | 35 ++ ignite/pkg/cosmosclient/consensus.go | 66 ++++ ignite/pkg/cosmosclient/cosmosclient.go | 323 ++++++++++-------- ignite/pkg/cosmosclient/txservice.go | 63 ++++ ignite/services/network/campaign.go | 6 +- ignite/services/network/client.go | 2 +- ignite/services/network/join.go | 2 +- ignite/services/network/join_test.go | 10 +- ignite/services/network/launch.go | 4 +- ignite/services/network/launch_test.go | 12 +- .../services/network/mocks/cosmos_client.go | 95 +----- ignite/services/network/network.go | 5 +- ignite/services/network/publish.go | 8 +- ignite/services/network/publish_test.go | 28 +- ignite/services/network/request.go | 2 +- ignite/services/network/reward.go | 2 +- ignite/services/network/reward_test.go | 4 +- integration/app.go | 236 +++++++++++++ integration/app/cmd_app_test.go | 66 ++-- integration/app/cmd_ibc_test.go | 73 ++-- integration/app/tx_test.go | 24 +- integration/chain/cache_test.go | 24 +- integration/chain/cmd_serve_test.go | 45 ++- integration/chain/config_test.go | 10 +- integration/cosmosgen/cosmosgen_test.go | 16 +- integration/env.go | 320 ++--------------- integration/exec.go | 109 ++++++ integration/faucet/faucet_test.go | 8 +- integration/list/cmd_list_test.go | 33 +- integration/map/cmd_map_test.go | 36 +- integration/node/cmd_query_bank_test.go | 280 +++++++++++++++ integration/node/cmd_query_tx_test.go | 97 ++++++ integration/node/cmd_tx_bank_send_test.go | 322 +++++++++++++++++ .../other_components/cmd_message_test.go | 23 +- .../other_components/cmd_query_test.go | 27 +- integration/simulation/simapp_test.go | 18 +- integration/single/cmd_singleton_test.go | 25 +- 67 files changed, 2232 insertions(+), 790 deletions(-) create mode 100644 ignite/cmd/node.go create mode 100644 ignite/cmd/node_query.go create mode 100644 ignite/cmd/node_query_bank.go create mode 100644 ignite/cmd/node_query_bank_balances.go create mode 100644 ignite/cmd/node_query_tx.go create mode 100644 ignite/cmd/node_tx.go create mode 100644 ignite/cmd/node_tx_bank.go create mode 100644 ignite/cmd/node_tx_bank_send.go create mode 100644 ignite/pkg/cosmosaccount/cosmosaccount_test.go create mode 100644 ignite/pkg/cosmosclient/bank.go create mode 100644 ignite/pkg/cosmosclient/consensus.go create mode 100644 ignite/pkg/cosmosclient/txservice.go create mode 100644 integration/app.go create mode 100644 integration/exec.go create mode 100644 integration/node/cmd_query_bank_test.go create mode 100644 integration/node/cmd_query_tx_test.go create mode 100644 integration/node/cmd_tx_bank_send_test.go 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() }