Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: base64 decoder for cw3 wasm messages #1731

Merged
merged 10 commits into from
Dec 27, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#1712](https://github.com/NibiruChain/nibiru/pull/1712) - refactor(inflation): turn inflation off by default
* [#1713](https://github.com/NibiruChain/nibiru/pull/1713) - chore(build-release): use new rocksdb libraries and link required libs
* [#1715](https://github.com/NibiruChain/nibiru/pull/1715) - fix(build): revert! to working build + cherry-picks
* [#1731](https://github.com/NibiruChain/nibiru/pull/1731) - feat(cli): add cli command to decode stargate base64 messages

#### Dapp modules: perp, spot, etc

Expand Down
126 changes: 126 additions & 0 deletions cmd/nibid/cmd/decode_base64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package cmd

import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdkcodec "github.com/cosmos/cosmos-sdk/codec"

wasmvm "github.com/CosmWasm/wasmvm/types"
)

// YieldStargateMsgs parses the JSON and sends wasmvm.StargateMsg objects to a channel
func YieldStargateMsgs(jsonBz []byte) ([]wasmvm.StargateMsg, error) {
var data interface{}
if err := json.Unmarshal(jsonBz, &data); err != nil {
return nil, err
}

Check warning on line 23 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L22-L23

Added lines #L22 - L23 were not covered by tests

var msgs []wasmvm.StargateMsg
parseStargateMsgs(data, &msgs)
return msgs, nil
}
Comment on lines +18 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YieldStargateMsgs function correctly unmarshals JSON bytes into an interface and then parses the messages. However, there is no error handling for the parseStargateMsgs function call. Ensure that any errors during parsing are properly handled.


func parseStargateMsgs(jsonData any, msgs *[]wasmvm.StargateMsg) {
switch v := jsonData.(type) {
case map[string]interface{}:
if typeURL, ok := v["type_url"].(string); ok {
if value, ok := v["value"].(string); ok {
*msgs = append(*msgs, wasmvm.StargateMsg{
TypeURL: typeURL,
Value: []byte(value),
})
}
}
for _, value := range v {
parseStargateMsgs(value, msgs)
}
case []interface{}:
for _, value := range v {
parseStargateMsgs(value, msgs)
}

Check warning on line 47 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L44-L47

Added lines #L44 - L47 were not covered by tests
}
}
Comment on lines +30 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recursive function parseStargateMsgs lacks error handling. If any of the type assertions fail, it will result in a runtime panic. It's important to handle these cases gracefully and return an error if the JSON structure does not meet the expected format.


type StargateMsgDecoded struct {
TypeURL string `json:"type_url"`
Value string `json:"value"`
}

func DecodeBase64StargateMsgs(
jsonBz []byte, context client.Context,
) (newSgMsgs []StargateMsgDecoded, err error) {
codec := context.Codec

var data interface{}
if err := json.Unmarshal(jsonBz, &data); err != nil {
return []StargateMsgDecoded{}, err
}

sgMsgs, err := YieldStargateMsgs(jsonBz)
if err != nil {
return
}

Check warning on line 69 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L68-L69

Added lines #L68 - L69 were not covered by tests
for _, sgMsg := range sgMsgs {
valueStr := string(sgMsg.Value)
value := strings.Replace(string(sgMsg.Value), `\"`, `"`, -1)
value = strings.Replace(value, `"{`, `{`, -1)
value = strings.Replace(value, `}"`, `}`, -1)

if _, err := base64.StdEncoding.DecodeString(valueStr); err == nil {
protoMsg, err := context.InterfaceRegistry.Resolve(sgMsg.TypeURL)
if err != nil {
return newSgMsgs, err
}

Check warning on line 80 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L79-L80

Added lines #L79 - L80 were not covered by tests

decodedBz, _ := base64.StdEncoding.Strict().DecodeString(string(sgMsg.Value))
concrete := protoMsg.(sdkcodec.ProtoMarshaler)

err = codec.Unmarshal(decodedBz, concrete)
if err != nil {
return newSgMsgs, err
}

Check warning on line 88 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L87-L88

Added lines #L87 - L88 were not covered by tests

outBytes, err := codec.MarshalJSON(concrete)
if err != nil {
return newSgMsgs, err
}

Check warning on line 93 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L92-L93

Added lines #L92 - L93 were not covered by tests

newSgMsgs = append(newSgMsgs, StargateMsgDecoded{sgMsg.TypeURL, string(outBytes)})
} else if _, err := json.Marshal(value); err == nil {
newSgMsgs = append(newSgMsgs, StargateMsgDecoded{sgMsg.TypeURL, string(sgMsg.Value)})
} else {
return newSgMsgs, fmt.Errorf(
"parse error: encountered wasmvm.StargateMsg with unexpected format: %s", sgMsg)
}

Check warning on line 101 in cmd/nibid/cmd/decode_base64.go

View check run for this annotation

Codecov / codecov/patch

cmd/nibid/cmd/decode_base64.go#L99-L101

Added lines #L99 - L101 were not covered by tests
}
return newSgMsgs, nil
}

// DecodeBase64Cmd creates a cobra command for base64 decoding.
func DecodeBase64Cmd(defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "base64-decode",
Short: "Decode a base64-encoded protobuf message",
Long: `Decode a base64-encoded protobuf message from JSON input.
The input should be a JSON object with 'type_url' and 'value' fields.`,
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)

outMessage, err := DecodeBase64StargateMsgs([]byte(args[0]), clientCtx)
fmt.Println(outMessage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would put if err == nil, print so that it only runs on success


return err
},
}

cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory")

return cmd
}
120 changes: 120 additions & 0 deletions cmd/nibid/cmd/decode_base64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cmd_test

import (
"context"
"testing"

"github.com/NibiruChain/nibiru/app"

"github.com/cometbft/cometbft/libs/log"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/server"
genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"

nibid "github.com/NibiruChain/nibiru/cmd/nibid/cmd"
)

func TestBase64Decode(t *testing.T) {
type TestCase struct {
name string
json_message string
expectError bool
}
Comment on lines +19 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TestCase struct is well-defined with relevant fields for the test's purpose. However, it's good practice to include a field for the expected result to compare against the actual result in tests, not just an error expectation.


executeTest := func(t *testing.T, testCase TestCase) {
tc := testCase
t.Run(tc.name, func(t *testing.T) {
home := t.TempDir()
logger := log.NewNopLogger()
cfg, err := genutiltest.CreateDefaultTendermintConfig(home)
require.NoError(t, err)

appCodec := app.MakeEncodingConfig().Marshaler
err = genutiltest.ExecInitCmd(
testModuleBasicManager, home, appCodec)
require.NoError(t, err)

serverCtx := server.NewContext(viper.New(), cfg, logger)
clientCtx := (client.Context{}.
WithCodec(appCodec).
WithHomeDir(home).
WithInterfaceRegistry(app.MakeEncodingConfig().InterfaceRegistry))

ctx := context.Background()
ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx)
ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx)

cmd := nibid.DecodeBase64Cmd(home)
cmd.SetArgs([]string{
tc.json_message,
})

if tc.expectError {
require.Error(t, cmd.ExecuteContext(ctx))
} else {
require.NoError(t, cmd.ExecuteContext(ctx))
}
})
}
Comment on lines +26 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executeTest function is well-structured and uses subtests, which is a good practice for clarity and isolation of test cases. However, the use of context.WithValue should be accompanied by a comment explaining why it's necessary to store clientCtx and serverCtx in the context, as this is not a common practice for contexts in tests.


testCases := []TestCase{
{
name: "valid message",
json_message: `
{
"stargate": {
"type_url": "/cosmos.staking.v1beta1.MsgUndelegate",
"value": "Cj9uaWJpMTdwOXJ6d25uZnhjanAzMnVuOXVnN3loaHpndGtodmw5amZrc3p0Z3c1dWg2OXdhYzJwZ3N5bjcwbmoSMm5pYml2YWxvcGVyMXdqNWtma25qa3BjNmpkMzByeHRtOHRweGZqZjd4cWx3eDM4YzdwGgwKBXVuaWJpEgMxMTE="
}
}`,
expectError: false,
},
{
name: "valid message",
json_message: `
{
"stargate": {
"type_url": "/cosmos.staking.v1beta1.MsgUndelegate",
"value": "Cj9uaWJpMTdwOXJ6d25uZnhjanAzMnVuOXVnN3loaHpndGtodmw5amZrc3p0Z3c1dWg2OXdhYzJwZ3N5bjcwbmoSMm5pYml2YWxvcGVyMXdqNWtma25qa3BjNmpkMzByeHRtOHRweGZqZjd4cWx3eDM4YzdwGgwKBXVuaWJpEgMxMTE="
},
"another": {
"type_url": "/cosmos.staking.v1beta1.MsgDelegate",
"value": {"delegator_address":"cosmos1eckjje8r8s48kv0pndgtwvehveedlzlnnshl3e", "validator_address":"cosmos1n6ndsc04xh2hqf506nhvhcggj0qwguf8ks06jj", "amount":{"denom":"unibi","amount":"42"} }
}
}`,
expectError: false,
},
{
name: "valid message",
json_message: `
{
"another": {
"type_url": "/cosmos.staking.v1beta1.MsgDelegate",
"value": "{\"delegator_address\":\"cosmos1eckjje8r8s48kv0pndgtwvehveedlzlnnshl3e\", \"validator_address\":\"cosmos1n6ndsc04xh2hqf506nhvhcggj0qwguf8ks06jj\", \"amount\":{\"denom\":\"unibi\",\"amount\":\"42\"} }"
}
}`,
expectError: false,
},
{
name: "empty message",
json_message: `
{

}`,
expectError: false,
},
{
name: "invalid json",
json_message: `

}`,
expectError: true,
},
}

for _, testCase := range testCases {
executeTest(t, testCase)
}
}
1 change: 1 addition & 0 deletions cmd/nibid/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig app.EncodingConfig) {
rootCmd.AddCommand(
InitCmd(app.ModuleBasics, app.DefaultNodeHome),
AddGenesisAccountCmd(app.DefaultNodeHome),
DecodeBase64Cmd(app.DefaultNodeHome),
tmcli.NewCompletionCmd(rootCmd, true),
testnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}),
debug.Cmd(),
Expand Down
Loading