Skip to content

Commit

Permalink
Add host cli to generate ica packet data (backport #2297) (#2422)
Browse files Browse the repository at this point in the history
* Add  host cli to generate ica packet data (#2297)

(cherry picked from commit 8f0e7d5)

# Conflicts:
#	CHANGELOG.md

* fix conflict

Co-authored-by: Cian Hatton <[email protected]>
Co-authored-by: Carlos Rodriguez <[email protected]>
Co-authored-by: colin axnér <[email protected]>
  • Loading branch information
4 people authored Sep 28, 2022
1 parent da9da72 commit 32bf4d0
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (apps/27-interchain-accounts) [\#2140](https://github.com/cosmos/ibc-go/pull/2140) Adding migration handler to ICS27 `controller` submodule to assert ownership of channel capabilities and set middleware enabled flag for existing channels. The ICS27 module consensus version has been bumped from 1 to 2.
* (apps/27-interchain-accounts) [\#2248](https://github.com/cosmos/ibc-go/pull/2248) Adding call to underlying app in `OnChanCloseConfirm` callback of the controller submodule and adding relevant unit tests.
* (apps/27-interchain-accounts) [\#2251](https://github.com/cosmos/ibc-go/pull/2251) Adding `msgServer` struct to controller submodule that embeds the `Keeper` struct.
* (apps/27-interchain-accounts) [\#2290](https://github.com/cosmos/ibc-go/pull/2290) Changed `DefaultParams` function in `host` submodule to allow all messages by default. Defined a constant named `AllowAllHostMsgs` for `host` module to keep wildcard "*" string which allows all messages.
* (apps/27-interchain-accounts) [\#2290](https://github.com/cosmos/ibc-go/pull/2290) Changed `DefaultParams` function in `host` submodule to allow all messages by default. Defined a constant named `AllowAllHostMsgs` for `host` module to keep wildcard "*" string which allows all messages.
* (apps/27-interchain-accounts) [\#2297](https://github.com/cosmos/ibc-go/pull/2297) Adding cli command to generate ICS27 packet data.

### Features

Expand Down
1 change: 1 addition & 0 deletions modules/apps/27-interchain-accounts/client/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func NewTxCmd() *cobra.Command {

icaTxCmd.AddCommand(
controllercli.NewTxCmd(),
hostcli.NewTxCmd(),
)

return icaTxCmd
Expand Down
18 changes: 18 additions & 0 deletions modules/apps/27-interchain-accounts/host/client/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/spf13/cobra"
)

Expand All @@ -20,3 +21,20 @@ func GetQueryCmd() *cobra.Command {

return queryCmd
}

// NewTxCmd creates and returns the tx command
func NewTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "host",
Short: "IBC interchain accounts host transaction subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}

cmd.AddCommand(
generatePacketDataCmd(),
)

return cmd
}
147 changes: 147 additions & 0 deletions modules/apps/27-interchain-accounts/host/client/cli/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package cli

import (
"encoding/json"
"fmt"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/spf13/cobra"

icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types"
)

const (
memoFlag string = "memo"
)

func generatePacketDataCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "generate-packet-data [message]",
Short: "Generates ICA packet data.",
Long: `generate-packet-data accepts a message string and serializes it
into packet data which is outputted to stdout. It can be used in conjunction with send-tx"
which submits pre-built packet data containing messages to be executed on the host chain.
`,
Example: fmt.Sprintf(`%s tx interchain-accounts host generate-packet-data '{
"@type":"/cosmos.bank.v1beta1.MsgSend",
"from_address":"cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz",
"to_address":"cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw",
"amount": [
{
"denom": "stake",
"amount": "1000"
}
]
}' --memo memo
%s tx interchain-accounts host generate-packet-data '[{
"@type":"/cosmos.bank.v1beta1.MsgSend",
"from_address":"cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz",
"to_address":"cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw",
"amount": [
{
"denom": "stake",
"amount": "1000"
}
]
},
{
"@type": "/cosmos.staking.v1beta1.MsgDelegate",
"delegator_address": "cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz",
"validator_address": "cosmosvaloper1qnk2n4nlkpw9xfqntladh74w6ujtulwnmxnh3k",
"amount": {
"denom": "stake",
"amount": "1000"
}
}]'`, version.AppName, version.AppName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

cdc := codec.NewProtoCodec(clientCtx.InterfaceRegistry)

memo, err := cmd.Flags().GetString(memoFlag)
if err != nil {
return err
}

packetDataBytes, err := generatePacketData(cdc, []byte(args[0]), memo)
if err != nil {
return err
}

cmd.Println(string(packetDataBytes))

return nil
},
}

cmd.Flags().String(memoFlag, "", "an optional memo to be included in the interchain account packet data")
return cmd
}

// generatePacketData takes in message bytes and a memo and serializes the message into an
// instance of InterchainAccountPacketData which is returned as bytes.
func generatePacketData(cdc *codec.ProtoCodec, msgBytes []byte, memo string) ([]byte, error) {
sdkMessages, err := convertBytesIntoSdkMessages(cdc, msgBytes)
if err != nil {
return nil, err
}

return generateIcaPacketDataFromSdkMessages(cdc, sdkMessages, memo)
}

// convertBytesIntoSdkMessages returns a list of sdk messages from bytes. The bytes can be in the form of a single
// message, or a json array of messages.
func convertBytesIntoSdkMessages(cdc *codec.ProtoCodec, msgBytes []byte) ([]sdk.Msg, error) {
var rawMessages []json.RawMessage
if err := json.Unmarshal(msgBytes, &rawMessages); err != nil {
// if we fail to unmarshal a list of messages, we assume we are just dealing with a single message.
// in this case we return a list of a single item.
var msg sdk.Msg
if err := cdc.UnmarshalInterfaceJSON(msgBytes, &msg); err != nil {
return nil, err
}

return []sdk.Msg{msg}, nil
}

sdkMessages := make([]sdk.Msg, len(rawMessages))
for i, anyJSON := range rawMessages {
var msg sdk.Msg
if err := cdc.UnmarshalInterfaceJSON(anyJSON, &msg); err != nil {
return nil, err
}

sdkMessages[i] = msg
}

return sdkMessages, nil
}

// generateIcaPacketDataFromSdkMessages generates ica packet data as bytes from a given set of sdk messages and a memo.
func generateIcaPacketDataFromSdkMessages(cdc *codec.ProtoCodec, sdkMessages []sdk.Msg, memo string) ([]byte, error) {
icaPacketDataBytes, err := icatypes.SerializeCosmosTx(cdc, sdkMessages)
if err != nil {
return nil, err
}

icaPacketData := icatypes.InterchainAccountPacketData{
Type: icatypes.EXECUTE_TX,
Data: icaPacketDataBytes,
Memo: memo,
}

if err := icaPacketData.ValidateBasic(); err != nil {
return nil, err
}

return cdc.MarshalJSON(&icaPacketData)
}
155 changes: 155 additions & 0 deletions modules/apps/27-interchain-accounts/host/client/cli/tx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cli

import (
"fmt"
"testing"

"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/stretchr/testify/require"

icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types"
)

const msgDelegateMessage = `{
"@type": "/cosmos.staking.v1beta1.MsgDelegate",
"delegator_address": "cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz",
"validator_address": "cosmosvaloper1qnk2n4nlkpw9xfqntladh74w6ujtulwnmxnh3k",
"amount": {
"denom": "stake",
"amount": "1000"
}
}`

const bankSendMessage = `{
"@type":"/cosmos.bank.v1beta1.MsgSend",
"from_address":"cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz",
"to_address":"cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw",
"amount": [
{
"denom": "stake",
"amount": "1000"
}
]
}`

var multiMsg = fmt.Sprintf("[ %s, %s ]", msgDelegateMessage, bankSendMessage)

func TestGeneratePacketData(t *testing.T) {
tests := []struct {
name string
memo string
expectedPass bool
message string
registerInterfaceFn func(registry codectypes.InterfaceRegistry)
assertionFn func(t *testing.T, msgs []sdk.Msg)
}{
{
name: "packet data generation succeeds (MsgDelegate & MsgSend)",
memo: "",
expectedPass: true,
message: multiMsg,
registerInterfaceFn: func(registry codectypes.InterfaceRegistry) {
stakingtypes.RegisterInterfaces(registry)
banktypes.RegisterInterfaces(registry)
},
assertionFn: func(t *testing.T, msgs []sdk.Msg) {
assertMsgDelegate(t, msgs[0])
assertMsgBankSend(t, msgs[1])
},
},
{
name: "packet data generation succeeds (MsgDelegate)",
memo: "non-empty-memo",
expectedPass: true,
message: msgDelegateMessage,
registerInterfaceFn: stakingtypes.RegisterInterfaces,
assertionFn: func(t *testing.T, msgs []sdk.Msg) {
assertMsgDelegate(t, msgs[0])
},
},
{
name: "packet data generation succeeds (MsgSend)",
memo: "non-empty-memo",
expectedPass: true,
message: bankSendMessage,
registerInterfaceFn: banktypes.RegisterInterfaces,
assertionFn: func(t *testing.T, msgs []sdk.Msg) {
assertMsgBankSend(t, msgs[0])
},
},
{
name: "empty memo is valid",
memo: "",
expectedPass: true,
message: msgDelegateMessage,
registerInterfaceFn: stakingtypes.RegisterInterfaces,
assertionFn: nil,
},
{
name: "invalid message string",
expectedPass: false,
message: "<invalid-message-body>",
},
}

for _, tc := range tests {
tc := tc
ir := codectypes.NewInterfaceRegistry()
if tc.registerInterfaceFn != nil {
tc.registerInterfaceFn(ir)
}

cdc := codec.NewProtoCodec(ir)

t.Run(tc.name, func(t *testing.T) {
bz, err := generatePacketData(cdc, []byte(tc.message), tc.memo)

if tc.expectedPass {
require.NoError(t, err)
require.NotNil(t, bz)

packetData := icatypes.InterchainAccountPacketData{}
err = cdc.UnmarshalJSON(bz, &packetData)
require.NoError(t, err)

require.Equal(t, icatypes.EXECUTE_TX, packetData.Type)
require.Equal(t, tc.memo, packetData.Memo)

data := packetData.Data
messages, err := icatypes.DeserializeCosmosTx(cdc, data)

require.NoError(t, err)
require.NotNil(t, messages)

if tc.assertionFn != nil {
tc.assertionFn(t, messages)
}
} else {
require.Error(t, err)
require.Nil(t, bz)
}
})
}
}

func assertMsgBankSend(t *testing.T, msg sdk.Msg) {
bankSendMsg, ok := msg.(*banktypes.MsgSend)
require.True(t, ok)
require.Equal(t, "cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz", bankSendMsg.FromAddress)
require.Equal(t, "cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw", bankSendMsg.ToAddress)
require.Equal(t, "stake", bankSendMsg.Amount.GetDenomByIndex(0))
require.Equal(t, uint64(1000), bankSendMsg.Amount[0].Amount.Uint64())
}

func assertMsgDelegate(t *testing.T, msg sdk.Msg) {
msgDelegate, ok := msg.(*stakingtypes.MsgDelegate)
require.True(t, ok)
require.Equal(t, "cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz", msgDelegate.DelegatorAddress)
require.Equal(t, "cosmosvaloper1qnk2n4nlkpw9xfqntladh74w6ujtulwnmxnh3k", msgDelegate.ValidatorAddress)
require.Equal(t, "stake", msgDelegate.Amount.Denom)
require.Equal(t, uint64(1000), msgDelegate.Amount.Amount.Uint64())
}

0 comments on commit 32bf4d0

Please sign in to comment.