Skip to content

Commit

Permalink
feat(bank/v2): Add MsgSend handler (#21736)
Browse files Browse the repository at this point in the history
  • Loading branch information
hieuvubk authored Sep 23, 2024
1 parent d273ae0 commit 04da382
Show file tree
Hide file tree
Showing 17 changed files with 2,239 additions and 93 deletions.
32 changes: 25 additions & 7 deletions runtime/v2/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,10 +459,8 @@ func (m *MM[T]) RunMigrations(ctx context.Context, fromVM appmodulev2.VersionMap
func (m *MM[T]) RegisterServices(app *App[T]) error {
for _, module := range m.modules {
// register msg + query
if services, ok := module.(hasServicesV1); ok {
if err := registerServices(services, app, protoregistry.GlobalFiles); err != nil {
return err
}
if err := registerServices(module, app, protoregistry.GlobalFiles); err != nil {
return err
}

// register migrations
Expand Down Expand Up @@ -594,7 +592,7 @@ func (m *MM[T]) assertNoForgottenModules(
return nil
}

func registerServices[T transaction.Tx](s hasServicesV1, app *App[T], registry *protoregistry.Files) error {
func registerServices[T transaction.Tx](s appmodulev2.AppModule, app *App[T], registry *protoregistry.Files) error {
c := &configurator{
grpcQueryDecoders: map[string]func() gogoproto.Message{},
stfQueryRouter: app.queryRouterBuilder,
Expand All @@ -603,8 +601,28 @@ func registerServices[T transaction.Tx](s hasServicesV1, app *App[T], registry *
err: nil,
}

if err := s.RegisterServices(c); err != nil {
return fmt.Errorf("unable to register services: %w", err)
if services, ok := s.(hasServicesV1); ok {
if err := services.RegisterServices(c); err != nil {
return fmt.Errorf("unable to register services: %w", err)
}
} else {
// If module not implement RegisterServices, register msg & query handler.
if module, ok := s.(appmodulev2.HasMsgHandlers); ok {
module.RegisterMsgHandlers(app.msgRouterBuilder)
}

if module, ok := s.(appmodulev2.HasQueryHandlers); ok {
module.RegisterQueryHandlers(app.queryRouterBuilder)
// TODO: query regist by RegisterQueryHandlers not in grpcQueryDecoders
if module, ok := s.(interface {
GetQueryDecoders() map[string]func() gogoproto.Message
}); ok {
decoderMap := module.GetQueryDecoders()
for path, decoder := range decoderMap {
app.GRPCMethodsToMessageMap[path] = decoder
}
}
}
}

if c.err != nil {
Expand Down
21 changes: 21 additions & 0 deletions simapp/v2/simdv2/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"cosmossdk.io/server/v2/cometbft"
"cosmossdk.io/server/v2/store"
banktypes "cosmossdk.io/x/bank/types"
bankv2types "cosmossdk.io/x/bank/v2/types"
stakingtypes "cosmossdk.io/x/staking/types"

"github.com/cosmos/cosmos-sdk/client"
Expand Down Expand Up @@ -402,6 +403,13 @@ func initGenFiles[T transaction.Tx](
}
appGenState[banktypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankGenState)

var bankV2GenState bankv2types.GenesisState
clientCtx.Codec.MustUnmarshalJSON(appGenState[bankv2types.ModuleName], &bankV2GenState)
if len(bankV2GenState.Balances) == 0 {
bankV2GenState = getBankV2GenesisFromV1(bankGenState)
}
appGenState[bankv2types.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankV2GenState)

appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ")
if err != nil {
return err
Expand Down Expand Up @@ -504,3 +512,16 @@ func writeFile(name, dir string, contents []byte) error {

return os.WriteFile(file, contents, 0o600)
}

// getBankV2GenesisFromV1 clones bank/v1 state to bank/v2
// since we not migrate yet
// TODO: Remove
func getBankV2GenesisFromV1(v1GenesisState banktypes.GenesisState) bankv2types.GenesisState {
var v2GenesisState bankv2types.GenesisState
for _, balance := range v1GenesisState.Balances {
v2Balance := bankv2types.Balance(balance)
v2GenesisState.Balances = append(v2GenesisState.Balances, v2Balance)
v2GenesisState.Supply = v2GenesisState.Supply.Add(balance.Coins...)
}
return v2GenesisState
}
59 changes: 59 additions & 0 deletions tests/systemtests/bankv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build system_test

package systemtests

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

func TestBankV2SendTxCmd(t *testing.T) {
// Currently only run with app v2
if !isV2() {
t.Skip()
}
// scenario: test bank send command
// given a running chain

sut.ResetChain(t)
cli := NewCLIWrapper(t, sut, verbose)

// get validator address
valAddr := gjson.Get(cli.Keys("keys", "list"), "1.address").String()
require.NotEmpty(t, valAddr)

// add new key
receiverAddr := cli.AddKey("account1")
denom := "stake"
sut.StartChain(t)

// query validator balance and make sure it has enough balance
var transferAmount int64 = 1000
raw := cli.CustomQuery("q", "bankv2", "balance", valAddr, denom)
valBalance := gjson.Get(raw, "balance.amount").Int()

require.Greater(t, valBalance, transferAmount, "not enough balance found with validator")

bankSendCmdArgs := []string{"tx", "bankv2", "send", valAddr, receiverAddr, fmt.Sprintf("%d%s", transferAmount, denom)}

// test valid transaction
rsp := cli.Run(append(bankSendCmdArgs, "--fees=1stake")...)
txResult, found := cli.AwaitTxCommitted(rsp)
require.True(t, found)
RequireTxSuccess(t, txResult)

// Check balance after send
valRaw := cli.CustomQuery("q", "bankv2", "balance", valAddr, denom)
valBalanceAfer := gjson.Get(valRaw, "balance.amount").Int()

// TODO: Make DeductFee ante handler work with bank/v2
require.Equal(t, valBalanceAfer, valBalance - transferAmount)

receiverRaw := cli.CustomQuery("q", "bankv2", "balance", receiverAddr, denom)
receiverBalance := gjson.Get(receiverRaw, "balance.amount").Int()
require.Equal(t, receiverBalance, transferAmount)

}
32 changes: 32 additions & 0 deletions x/bank/proto/cosmos/bank/v2/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,43 @@ package cosmos.bank.v2;
import "gogoproto/gogo.proto";
import "cosmos/bank/v2/bank.proto";
import "amino/amino.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "cosmossdk.io/x/bank/v2/types";

// GenesisState defines the bank/v2 module's genesis state.
message GenesisState {
// params defines all the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true];

// balances is an array containing the balances of all the accounts.
repeated Balance balances = 2 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true];

// supply represents the total supply. If it is left empty, then supply will be calculated based on the provided
// balances. Otherwise, it will be used to validate that the sum of the balances equals this amount.
repeated cosmos.base.v1beta1.Coin supply = 3 [
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
}

// Balance defines an account address and balance pair used in the bank module's
// genesis state.
message Balance {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

// address is the address of the balance holder.
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// coins defines the different coins this balance holds.
repeated cosmos.base.v1beta1.Coin coins = 2 [
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
}
20 changes: 20 additions & 0 deletions x/bank/proto/cosmos/bank/v2/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package cosmos.bank.v2;
import "gogoproto/gogo.proto";
import "amino/amino.proto";
import "cosmos/bank/v2/bank.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "cosmossdk.io/x/bank/v2/types";

Expand All @@ -14,4 +16,22 @@ message QueryParamsRequest {}
message QueryParamsResponse {
// params provides the parameters of the bank module.
Params params = 1 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true];
}

// QueryBalanceRequest is the request type for the Query/Balance RPC method.
message QueryBalanceRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

// address is the address to query balances for.
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// denom is the coin denom to query balances for.
string denom = 2;
}

// QueryBalanceResponse is the response type for the Query/Balance RPC method.
message QueryBalanceResponse {
// balance is the balance of the coin.
cosmos.base.v1beta1.Coin balance = 1;
}
41 changes: 40 additions & 1 deletion x/bank/proto/cosmos/bank/v2/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "cosmos/bank/v2/bank.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/msg/v1/msg.proto";
import "amino/amino.proto";
import "cosmos/base/v1beta1/coin.proto";

option go_package = "cosmossdk.io/x/bank/v2/types";

Expand All @@ -23,4 +24,42 @@ message MsgUpdateParams {
}

// MsgUpdateParamsResponse defines the response structure for executing a MsgUpdateParams message.
message MsgUpdateParamsResponse {}
message MsgUpdateParamsResponse {}

// MsgSend represents a message to send coins from one account to another.
message MsgSend {
option (cosmos.msg.v1.signer) = "from_address";
option (amino.name) = "cosmos-sdk/x/bank/v2/MsgSend";

string from_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string to_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
repeated cosmos.base.v1beta1.Coin amount = 3 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

// MsgSendResponse defines the response structure for executing a MsgSend message.
message MsgSendResponse {}

// MsgMint is the Msg/Mint request type.
message MsgMint {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "cosmos-sdk/x/bank/v2/MsgMint";

// authority is the address that controls the module (defaults to x/gov unless overwritten).
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

string to_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
repeated cosmos.base.v1beta1.Coin amount = 3 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

// MsgMint defines the response structure for executing a MsgMint message.
message MsgMintResponse {}
79 changes: 79 additions & 0 deletions x/bank/v2/client/cli/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cli

import (
"errors"

gogoproto "github.com/cosmos/gogoproto/proto"
"github.com/spf13/cobra"

"cosmossdk.io/x/bank/v2/types"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
)

const (
FlagDenom = "denom"
)

// GetQueryCmd returns the parent command for all x/bank CLi query commands. The
// provided clientCtx should have, at a minimum, a verifier, Tendermint RPC client,
// and marshaler set.
func GetQueryCmd() *cobra.Command {
cmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the bank module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}

cmd.AddCommand(
GetBalanceCmd(),
)

return cmd
}

func GetBalanceCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "balance [address] [denom]",
Short: "Query an account balance by address and denom",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

denom := args[1]
if denom == "" {
return errors.New("empty denom")
}

addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}

ctx := cmd.Context()

req := types.NewQueryBalanceRequest(addr.String(), denom)
out := new(types.QueryBalanceResponse)

err = clientCtx.Invoke(ctx, gogoproto.MessageName(&types.QueryBalanceRequest{}), req, out)
if err != nil {
return err
}

return clientCtx.PrintProto(out)
},
}

cmd.Flags().String(FlagDenom, "", "The specific balance denomination to query for")
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "all balances")

return cmd
}
Loading

0 comments on commit 04da382

Please sign in to comment.