diff --git a/changelog.md b/changelog.md index 8b7cc0e520..8098d8b3df 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ - [#3839](https://github.com/ignite/cli/pull/3839) New structure for app scaffolding - [#3835](https://github.com/ignite/cli/pull/3835) Add `--minimal` flag to `scaffold chain` to scaffold a chain with the least amount of sdk modules +- [#3820](https://github.com/ignite/cli/pull/3820) Add integration tests for IBC chains ### Changes diff --git a/ignite/pkg/goanalysis/goanalysis.go b/ignite/pkg/goanalysis/goanalysis.go index 6e3846c173..36f16a472f 100644 --- a/ignite/pkg/goanalysis/goanalysis.go +++ b/ignite/pkg/goanalysis/goanalysis.go @@ -279,3 +279,62 @@ func createUnderscoreImport(imp string) *ast.ImportSpec { }, } } + +// ReplaceCode replace a function implementation into a package path. The method will find +// the method signature and re-write the method implementation based in the new function. +func ReplaceCode(pkgPath, oldFunctionName, newFunction string) (err error) { + absPath, err := filepath.Abs(pkgPath) + if err != nil { + return err + } + + fileSet := token.NewFileSet() + all, err := parser.ParseDir(fileSet, absPath, func(os.FileInfo) bool { return true }, parser.ParseComments) + if err != nil { + return err + } + + for _, pkg := range all { + for _, f := range pkg.Files { + found := false + ast.Inspect(f, func(n ast.Node) bool { + if funcDecl, ok := n.(*ast.FuncDecl); ok { + // Check if the function has the name you want to replace. + if funcDecl.Name.Name == oldFunctionName { + // Replace the function body with the replacement code. + replacementExpr, err := parser.ParseExpr(newFunction) + if err != nil { + return false + } + funcDecl.Body = &ast.BlockStmt{List: []ast.Stmt{ + &ast.ExprStmt{X: replacementExpr}, + }} + found = true + return false + } + } + return true + }) + if err != nil { + return err + } + if !found { + continue + } + filePath := fileSet.Position(f.Package).Filename + outFile, err := os.Create(filePath) + if err != nil { + return err + } + + // Format and write the modified AST to the output file. + if err := format.Node(outFile, fileSet, f); err != nil { + return err + } + if err := outFile.Close(); err != nil { + return err + } + } + } + return nil +} diff --git a/ignite/pkg/goanalysis/goanalysis_test.go b/ignite/pkg/goanalysis/goanalysis_test.go index e19899b619..4d1297e3b0 100644 --- a/ignite/pkg/goanalysis/goanalysis_test.go +++ b/ignite/pkg/goanalysis/goanalysis_test.go @@ -503,3 +503,77 @@ func TestUpdateInitImports(t *testing.T) { }) } } + +func TestReplaceCode(t *testing.T) { + var ( + newFunction = `package test +func NewMethod1() { + n := "test new method" + bla := fmt.Sprintf("test new - %s", n) + fmt.Println(bla) +}` + rollback = `package test +func NewMethod1() { + foo := 100 + bar := fmt.Sprintf("test - %d", foo) + fmt.Println(bar) +}` + ) + + type args struct { + path string + oldFunctionName string + newFunction string + } + tests := []struct { + name string + args args + err error + }{ + { + name: "function fooTest", + args: args{ + path: "testdata", + oldFunctionName: "fooTest", + newFunction: newFunction, + }, + }, + { + name: "function BazTest", + args: args{ + path: "testdata", + oldFunctionName: "BazTest", + newFunction: newFunction, + }, + }, + { + name: "function invalidFunction", + args: args{ + path: "testdata", + oldFunctionName: "invalidFunction", + newFunction: newFunction, + }, + }, + { + name: "invalid path", + args: args{ + path: "invalid_path", + oldFunctionName: "invalidPath", + newFunction: newFunction, + }, + err: os.ErrNotExist, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := goanalysis.ReplaceCode(tt.args.path, tt.args.oldFunctionName, tt.args.newFunction) + if tt.err != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + require.NoError(t, goanalysis.ReplaceCode(tt.args.path, tt.args.oldFunctionName, rollback)) + }) + } +} diff --git a/ignite/pkg/goanalysis/testdata/replace.go b/ignite/pkg/goanalysis/testdata/replace.go new file mode 100644 index 0000000000..20c1a12c70 --- /dev/null +++ b/ignite/pkg/goanalysis/testdata/replace.go @@ -0,0 +1,16 @@ +package testdata + +import "fmt" + +func fooTest() { + n := "test new method" + bla := fmt.Sprintf("test new - %s", n) + fmt. + Println(bla) +} + +func BazTest() { + foo := 100 + bar := fmt.Sprintf("test - %d", foo) + fmt.Println(bar) +} diff --git a/ignite/pkg/goenv/goenv.go b/ignite/pkg/goenv/goenv.go index 17bc304280..954974574b 100644 --- a/ignite/pkg/goenv/goenv.go +++ b/ignite/pkg/goenv/goenv.go @@ -55,3 +55,8 @@ func GoModCache() string { } return filepath.Join(build.Default.GOPATH, modDir) } + +// GoPath returns the go path. +func GoPath() string { + return os.Getenv(GOPATH) +} diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush index 8bb54e2f73..8058636f52 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush @@ -22,6 +22,7 @@ import ( authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/cosmos/cosmos-sdk/x/crisis" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + ibccmd "github.com/cosmos/ibc-go/v8/modules/core/client/cli" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -87,6 +88,7 @@ func queryCommand() *cobra.Command { server.QueryBlocksCmd(), authcmd.QueryTxCmd(), server.QueryBlockResultsCmd(), + ibccmd.GetQueryCmd(), ) cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") @@ -113,6 +115,7 @@ func txCommand() *cobra.Command { authcmd.GetEncodeCommand(), authcmd.GetDecodeCommand(), authcmd.GetSimulateCmd(), + ibccmd.GetTxCmd(), ) cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") diff --git a/ignite/templates/ibc/files/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush b/ignite/templates/ibc/files/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush index 647cfda4c8..762e279b0d 100644 --- a/ignite/templates/ibc/files/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush +++ b/ignite/templates/ibc/files/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush @@ -14,6 +14,7 @@ import ( var _ = strconv.Itoa(0) +// CmdSend<%= packetName.UpperCamel %>() returns the <%= packetName.UpperCamel %> send packet command. // This command does not use AutoCLI because it gives a better UX to do not. func CmdSend<%= packetName.UpperCamel %>() *cobra.Command { flagPacketTimeoutTimestamp := "packet-timeout-timestamp" diff --git a/integration/app.go b/integration/app.go index 29aceb8dbd..2af4f6a816 100644 --- a/integration/app.go +++ b/integration/app.go @@ -118,6 +118,10 @@ func (a App) SourcePath() string { return a.path } +func (a *App) SetHomePath(homePath string) { + a.homePath = homePath +} + func (a *App) SetConfigPath(path string) { a.configPath = path } diff --git a/integration/ibc/cmd_relayer_test.go b/integration/ibc/cmd_relayer_test.go new file mode 100644 index 0000000000..30fb207fd9 --- /dev/null +++ b/integration/ibc/cmd_relayer_test.go @@ -0,0 +1,635 @@ +//go:build !relayer + +package ibc_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + "github.com/ignite/cli/v28/ignite/config/chain" + "github.com/ignite/cli/v28/ignite/config/chain/base" + v1 "github.com/ignite/cli/v28/ignite/config/chain/v1" + "github.com/ignite/cli/v28/ignite/pkg/availableport" + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner" + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/v28/ignite/pkg/goanalysis" + yamlmap "github.com/ignite/cli/v28/ignite/pkg/yaml" + envtest "github.com/ignite/cli/v28/integration" +) + +const ( + relayerMnemonic = "great immense still pill defense fetch pencil slow purchase symptom speed arm shoot fence have divorce cigar rapid hen vehicle pear evolve correct nerve" +) + +var ( + bobName = "bob" + marsConfig = v1.Config{ + Config: base.Config{ + Version: 1, + Build: base.Build{ + Proto: base.Proto{ + Path: "proto", + ThirdPartyPaths: []string{"third_party/proto", "proto_vendor"}, + }, + }, + Accounts: []base.Account{ + { + Name: "alice", + Coins: []string{"100000000000token", "10000000000000000000stake"}, + Mnemonic: "slide moment original seven milk crawl help text kick fluid boring awkward doll wonder sure fragile plate grid hard next casual expire okay body", + }, + { + Name: "bob", + Coins: []string{"100000000000token", "10000000000000000000stake"}, + Mnemonic: "trap possible liquid elite embody host segment fantasy swim cable digital eager tiny broom burden diary earn hen grow engine pigeon fringe claim program", + }, + { + Name: "relayer", + Coins: []string{"100000000000token", "1000000000000000000000stake"}, + Mnemonic: relayerMnemonic, + }, + }, + Faucet: base.Faucet{ + Name: &bobName, + Coins: []string{"500token", "100000000stake"}, + Host: ":4501", + }, + Genesis: yamlmap.Map{"chain_id": "mars-1"}, + }, + Validators: []v1.Validator{ + { + Name: "alice", + Bonded: "100000000stake", + Client: yamlmap.Map{"keyring-backend": keyring.BackendTest}, + App: yamlmap.Map{ + "api": yamlmap.Map{"address": ":1318"}, + "grpc": yamlmap.Map{"address": ":9092"}, + "grpc-web": yamlmap.Map{"address": ":9093"}, + }, + Config: yamlmap.Map{ + "p2p": yamlmap.Map{"laddr": ":26658"}, + "rpc": yamlmap.Map{"laddr": ":26658", "pprof_laddr": ":6061"}, + }, + Home: "$HOME/.mars", + }, + }, + } + earthConfig = v1.Config{ + Config: base.Config{ + Version: 1, + Build: base.Build{ + Proto: base.Proto{ + Path: "proto", + ThirdPartyPaths: []string{"third_party/proto", "proto_vendor"}, + }, + }, + Accounts: []base.Account{ + { + Name: "alice", + Coins: []string{"100000000000token", "10000000000000000000stake"}, + Mnemonic: "slide moment original seven milk crawl help text kick fluid boring awkward doll wonder sure fragile plate grid hard next casual expire okay body", + }, + { + Name: "bob", + Coins: []string{"100000000000token", "10000000000000000000stake"}, + Mnemonic: "trap possible liquid elite embody host segment fantasy swim cable digital eager tiny broom burden diary earn hen grow engine pigeon fringe claim program", + }, + { + Name: "relayer", + Coins: []string{"100000000000token", "1000000000000000000000stake"}, + Mnemonic: relayerMnemonic, + }, + }, + Faucet: base.Faucet{ + Name: &bobName, + Coins: []string{"500token", "100000000stake"}, + Host: ":4500", + }, + Genesis: yamlmap.Map{"chain_id": "earth-1"}, + }, + Validators: []v1.Validator{ + { + Name: "alice", + Bonded: "100000000stake", + Client: yamlmap.Map{"keyring-backend": keyring.BackendTest}, + App: yamlmap.Map{ + "api": yamlmap.Map{"address": ":1317"}, + "grpc": yamlmap.Map{"address": ":9090"}, + "grpc-web": yamlmap.Map{"address": ":9091"}, + }, + Config: yamlmap.Map{ + "p2p": yamlmap.Map{"laddr": ":26656"}, + "rpc": yamlmap.Map{"laddr": ":26656", "pprof_laddr": ":6060"}, + }, + Home: "$HOME/.earth", + }, + }, + } + + nameSendIbcPost = "SendIbcPost" + funcSendIbcPost = `package keeper +func (k msgServer) SendIbcPost(goCtx context.Context, msg *types.MsgSendIbcPost) (*types.MsgSendIbcPostResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + // Construct the packet + var packet types.IbcPostPacketData + packet.Title = msg.Title + packet.Content = msg.Content + // Transmit the packet + _, err := k.TransmitIbcPostPacket( + ctx, + packet, + msg.Port, + msg.ChannelID, + clienttypes.ZeroHeight(), + msg.TimeoutTimestamp, + ) + return &types.MsgSendIbcPostResponse{}, err +}` + + nameOnRecvIbcPostPacket = "OnRecvIbcPostPacket" + funcOnRecvIbcPostPacket = `package keeper +func (k Keeper) OnRecvIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData) (packetAck types.IbcPostPacketAck, err error) { + // validate packet data upon receiving + if err := data.ValidateBasic(); err != nil { + return packetAck, err + } + packetAck.PostId = k.AppendPost(ctx, types.Post{Title: data.Title, Content: data.Content}) + return packetAck, nil +}` + + nameOnAcknowledgementIbcPostPacket = "OnAcknowledgementIbcPostPacket" + funcOnAcknowledgementIbcPostPacket = `package keeper +func (k Keeper) OnAcknowledgementIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData, ack channeltypes.Acknowledgement) error { + switch dispatchedAck := ack.Response.(type) { + case *channeltypes.Acknowledgement_Error: + // We will not treat acknowledgment error in this tutorial + return nil + case *channeltypes.Acknowledgement_Result: + // Decode the packet acknowledgment + var packetAck types.IbcPostPacketAck + if err := types.ModuleCdc.UnmarshalJSON(dispatchedAck.Result, &packetAck); err != nil { + // The counter-party module doesn't implement the correct acknowledgment format + return errors.New("cannot unmarshal acknowledgment") + } + + k.AppendSentPost(ctx, + types.SentPost{ + PostId: packetAck.PostId, + Title: data.Title, + Chain: packet.DestinationPort + "-" + packet.DestinationChannel, + }, + ) + return nil + default: + return errors.New("the counter-party module does not implement the correct acknowledgment format") + } +}` + + nameOnTimeoutIbcPostPacket = "OnTimeoutIbcPostPacket" + funcOnTimeoutIbcPostPacket = `package keeper +func (k Keeper) OnTimeoutIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData) error { + k.AppendTimeoutPost(ctx, + types.TimeoutPost{ + Title: data.Title, + Chain: packet.DestinationPort + "-" + packet.DestinationChannel, + }, + ) + return nil +}` +) + +type ( + QueryChannels struct { + Channels []struct { + ChannelId string `json:"channel_id"` + ConnectionHops []string `json:"connection_hops"` + Counterparty struct { + ChannelId string `json:"channel_id"` + PortId string `json:"port_id"` + } `json:"counterparty"` + Ordering string `json:"ordering"` + PortId string `json:"port_id"` + State string `json:"state"` + Version string `json:"version"` + } `json:"channels"` + } + + QueryBalances struct { + Balances sdk.Coins `json:"balances"` + } +) + +func runChain( + t *testing.T, + ctx context.Context, + env envtest.Env, + app envtest.App, + cfg v1.Config, + tmpDir string, + ports []uint, +) (api, rpc, grpc, faucet string) { + t.Helper() + if len(ports) < 7 { + t.Fatalf("invalid number of ports %d", len(ports)) + } + + var ( + chainID = cfg.Genesis["chain_id"].(string) + chainPath = filepath.Join(tmpDir, chainID) + homePath = filepath.Join(chainPath, "home") + cfgPath = filepath.Join(chainPath, chain.ConfigFilenames[0]) + ) + require.NoError(t, os.MkdirAll(chainPath, os.ModePerm)) + + genAddr := func(port uint) string { + return fmt.Sprintf(":%d", port) + } + + cfg.Validators[0].Home = homePath + + cfg.Faucet.Host = genAddr(ports[0]) + cfg.Validators[0].App["api"] = yamlmap.Map{"address": genAddr(ports[1])} + cfg.Validators[0].App["grpc"] = yamlmap.Map{"address": genAddr(ports[2])} + cfg.Validators[0].App["grpc-web"] = yamlmap.Map{"address": genAddr(ports[3])} + cfg.Validators[0].Config["p2p"] = yamlmap.Map{"laddr": genAddr(ports[4])} + cfg.Validators[0].Config["rpc"] = yamlmap.Map{ + "laddr": genAddr(ports[5]), + "pprof_laddr": genAddr(ports[6]), + } + + file, err := os.Create(cfgPath) + require.NoError(t, err) + require.NoError(t, yaml.NewEncoder(file).Encode(cfg)) + require.NoError(t, file.Close()) + + app.SetConfigPath(cfgPath) + app.SetHomePath(homePath) + go func() { + env.Must(app.Serve("should serve chain", envtest.ExecCtx(ctx))) + }() + + genHTTPAddr := func(port uint) string { + return fmt.Sprintf("http://127.0.0.1:%d", port) + } + return genHTTPAddr(ports[1]), genHTTPAddr(ports[5]), genHTTPAddr(ports[2]), genHTTPAddr(ports[0]) +} + +func TestBlogIBC(t *testing.T) { + var ( + env = envtest.New(t) + app = env.Scaffold("github.com/test/planet") + ctx = env.Ctx() + tmpDir = t.TempDir() + ) + ctx, cancel := context.WithCancel(ctx) + t.Cleanup(func() { + cancel() + time.Sleep(5 * time.Second) + require.NoError(t, os.RemoveAll(tmpDir)) + }) + + env.Must(env.Exec("create an IBC module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "module", + "blog", + "--ibc", + "--require-registration", + "--yes", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a post type list in an IBC module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "list", + "post", + "title", + "content", + "--no-message", + "--module", + "blog", + "--yes", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a sentPost type list in an IBC module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "list", + "sentPost", + "postID:uint", + "title", + "chain", + "--no-message", + "--module", + "blog", + "--yes", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a timeoutPost type list in an IBC module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "list", + "timeoutPost", + "title", + "chain", + "--no-message", + "--module", + "blog", + "--yes", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a ibcPost package in an IBC module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "packet", + "ibcPost", + "title", + "content", + "--ack", + "postID:uint", + "--module", + "blog", + "--yes", + ), + step.Workdir(app.SourcePath()), + )), + )) + + blogKeeperPath := filepath.Join(app.SourcePath(), "x/blog/keeper") + require.NoError(t, goanalysis.ReplaceCode( + blogKeeperPath, + nameSendIbcPost, + funcSendIbcPost, + )) + require.NoError(t, goanalysis.ReplaceCode( + blogKeeperPath, + nameOnRecvIbcPostPacket, + funcOnRecvIbcPostPacket, + )) + require.NoError(t, goanalysis.ReplaceCode( + blogKeeperPath, + nameOnAcknowledgementIbcPostPacket, + funcOnAcknowledgementIbcPostPacket, + )) + require.NoError(t, goanalysis.ReplaceCode( + blogKeeperPath, + nameOnTimeoutIbcPostPacket, + funcOnTimeoutIbcPostPacket, + )) + + // serve both chains. + ports, err := availableport.Find( + 14, + availableport.WithMinPort(4000), + availableport.WithMaxPort(5000), + ) + require.NoError(t, err) + earthAPI, earthRPC, earthGRPC, earthFaucet := runChain(t, ctx, env, app, earthConfig, tmpDir, ports[:7]) + earthChainID := earthConfig.Genesis["chain_id"].(string) + earthHome := earthConfig.Validators[0].Home + marsAPI, marsRPC, marsGRPC, marsFaucet := runChain(t, ctx, env, app, marsConfig, tmpDir, ports[7:]) + marsChainID := marsConfig.Genesis["chain_id"].(string) + marsHome := marsConfig.Validators[0].Home + + // check the chains is up + stepsCheckChains := step.NewSteps( + step.New( + step.Exec( + app.Binary(), + "config", + "output", "json", + ), + step.PreExec(func() error { + if err := env.IsAppServed(ctx, earthAPI); err != nil { + return err + } + return env.IsAppServed(ctx, marsAPI) + }), + ), + ) + env.Exec("waiting the chain is up", stepsCheckChains, envtest.ExecRetry()) + + // ibc relayer. + env.Must(env.Exec("install the hermes relayer app", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "app", + "install", + "-g", + // filepath.Join(goenv.GoPath(), "src/github.com/ignite/apps/hermes"), // Local path for test proposals + "github.com/ignite/apps/hermes", + ), + )), + )) + + env.Must(env.Exec("configure the hermes relayer app", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "relayer", + "hermes", + "configure", + earthChainID, + earthRPC, + earthGRPC, + marsChainID, + marsRPC, + marsGRPC, + "--chain-a-faucet", earthFaucet, + "--chain-b-faucet", marsFaucet, + "--generate-wallets", + "--overwrite-config", + ), + )), + )) + + go func() { + env.Must(env.Exec("run the hermes relayer", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "relayer", "hermes", "start", earthChainID, marsChainID), + )), + envtest.ExecCtx(ctx), + )) + }() + time.Sleep(3 * time.Second) + + var ( + queryOutput = &bytes.Buffer{} + queryResponse QueryChannels + ) + env.Must(env.Exec("verify if the channel was created", step.NewSteps( + step.New( + step.Stdout(queryOutput), + step.Exec( + app.Binary(), + "q", + "ibc", + "channel", + "channels", + "--node", earthRPC, + "--log_format", "json", + "--output", "json", + ), + step.PostExec(func(execErr error) error { + if execErr != nil { + return execErr + } + if err := json.Unmarshal(queryOutput.Bytes(), &queryResponse); err != nil { + return fmt.Errorf("unmarshling tx response: %w", err) + } + if len(queryResponse.Channels) == 0 || + len(queryResponse.Channels[0].ConnectionHops) == 0 { + return fmt.Errorf("channel not found") + } + if queryResponse.Channels[0].State != "STATE_OPEN" { + return fmt.Errorf("channel is not open") + } + return nil + }), + ), + ))) + + var ( + sender = "alice" + receiverAddr = "cosmos1nrksk5swk6lnmlq670a8kwxmsjnu0ezqts39sa" + txOutput = &bytes.Buffer{} + txResponse struct { + Code int + RawLog string `json:"raw_log"` + TxHash string `json:"txhash"` + } + ) + + stepsTx := step.NewSteps( + step.New( + step.Stdout(txOutput), + step.Exec( + app.Binary(), + "tx", + "ibc-transfer", + "transfer", + "transfer", + "channel-0", + receiverAddr, + "100000stake", + "--from", sender, + "--node", earthRPC, + "--home", earthHome, + "--chain-id", earthChainID, + "--output", "json", + "--log_format", "json", + "--keyring-backend", "test", + "--yes", + ), + step.PostExec(func(execErr error) error { + if execErr != nil { + return execErr + } + if err := json.Unmarshal(txOutput.Bytes(), &txResponse); err != nil { + return fmt.Errorf("unmarshling tx response: %w", err) + } + return cmdrunner.New().Run(ctx, step.New( + step.Exec( + app.Binary(), + "q", + "tx", + txResponse.TxHash, + "--node", earthRPC, + "--home", earthHome, + "--chain-id", earthChainID, + "--output", "json", + "--log_format", "json", + ), + step.Stdout(txOutput), + step.PreExec(func() error { + txOutput.Reset() + return nil + }), + step.PostExec(func(execErr error) error { + if execErr != nil { + return execErr + } + if err := json.Unmarshal(txOutput.Bytes(), &txResponse); err != nil { + return err + } + return nil + }), + )) + }), + ), + ) + if !env.Exec("send an IBC transfer", stepsTx, envtest.ExecRetry()) { + t.FailNow() + } + require.Equal(t, 0, txResponse.Code, + "tx failed code=%d log=%s", txResponse.Code, txResponse.RawLog) + + var ( + balanceOutput = &bytes.Buffer{} + balanceResponse QueryBalances + ) + env.Must(env.Exec("check ibc balance", step.NewSteps( + step.New( + step.Stdout(balanceOutput), + step.Exec( + app.Binary(), + "q", + "bank", + "balances", + receiverAddr, + "--node", marsRPC, + "--home", marsHome, + "--log_format", "json", + "--output", "json", + ), + step.PostExec(func(execErr error) error { + if execErr != nil { + return execErr + } + if err := json.Unmarshal(balanceOutput.Bytes(), &balanceResponse); err != nil { + return fmt.Errorf("unmarshalling tx response: %w", err) + } + if balanceResponse.Balances.Empty() { + return fmt.Errorf("empty balances") + } + if !strings.HasPrefix(balanceResponse.Balances[0].Denom, "ibc/") { + return fmt.Errorf("invalid ibc balance: %v", balanceResponse.Balances[0]) + } + return nil + }), + ), + ))) + + // TODO test ibc using the blog post methods: + // step.Exec(app.Binary(), "tx", "blog", "send-ibc-post", "transfer", "channel-0", "Hello", "Hello_Mars-Alice_from_Earth", "--chain-id", earthChainID, "--from", "alice", "--node", earthGRPC, "--output", "json", "--log_format", "json", "--yes") + // TODO test ibc using the hermes ft-transfer: + // step.Exec(envtest.IgniteApp, "relayer", "hermes", "exec", "--", "--config", earthConfig, "tx", "ft-transfer", "--timeout-seconds", "1000", "--dst-chain", earthChainID, "--src-chain", marsChainID, "--src-port", "transfer", "--src-channel", "channel-0", "--amount", "100000", "--denom", "stake", "--output", "json", "--log_format", "json", "--yes") +}