Skip to content

Commit

Permalink
feat(accounts): Allow funds to be sent to accounts on init and execute (
Browse files Browse the repository at this point in the history
  • Loading branch information
testinginprod authored Feb 8, 2024
1 parent 8de3966 commit 5be33f5
Show file tree
Hide file tree
Showing 19 changed files with 1,124 additions and 265 deletions.
261 changes: 211 additions & 50 deletions api/cosmos/accounts/testing/counter/v1/counter.pulsar.go

Large diffs are not rendered by default.

494 changes: 405 additions & 89 deletions api/cosmos/accounts/v1/tx.pulsar.go

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions core/appmodule/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"google.golang.org/grpc"
"google.golang.org/protobuf/runtime/protoiface"
)

// AppModule is a tag interface for app module implementations to use as a basis
Expand Down Expand Up @@ -86,3 +87,16 @@ type HasEndBlocker interface {
// a block.
EndBlock(context.Context) error
}

// MsgHandlerRouter is implemented by the runtime provider.
type MsgHandlerRouter interface {
// RegisterHandler is called by modules to register msg handler functions.
RegisterHandler(name string, handler func(ctx context.Context, msg protoiface.MessageV1) (msgResp protoiface.MessageV1, err error))
}

// HasMsgHandler is implemented by modules that instead of exposing msg server expose
// a set of handlers.
type HasMsgHandler interface {
// RegisterMsgHandlers is implemented by the module that will register msg handlers.
RegisterMsgHandlers(router MsgHandlerRouter)
}
6 changes: 6 additions & 0 deletions proto/cosmos/accounts/testing/counter/v1/counter.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ package cosmos.accounts.testing.counter.v1;

option go_package = "cosmossdk.io/x/accounts/testing/counter/v1";

import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";

// MsgInit defines a message which initializes the counter with a given amount.
message MsgInit {
// initial_value is the initial amount to set the counter to.
Expand Down Expand Up @@ -39,6 +42,9 @@ message MsgTestDependenciesResponse {
uint64 before_gas = 3;
// after_gas is used to test gas meter increasing.
uint64 after_gas = 4;
// funds reports the funds from the implementation.Funds method.
repeated cosmos.base.v1beta1.Coin funds = 5
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// QueryCounterRequest is used to query the counter value.
Expand Down
10 changes: 10 additions & 0 deletions proto/cosmos/accounts/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ option go_package = "cosmossdk.io/x/accounts/v1";
import "google/protobuf/any.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos/accounts/v1/account_abstraction.proto";
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";

// Msg defines the Msg service for the x/accounts module.
service Msg {
Expand All @@ -33,6 +35,10 @@ message MsgInit {
string account_type = 2;
// message is the message to be sent to the account.
google.protobuf.Any message = 3;
// funds contains the coins that the account wants to
// send alongside the request.
repeated cosmos.base.v1beta1.Coin funds = 4
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// MsgInitResponse defines the Create response type for the Msg/Create RPC method.
Expand All @@ -52,6 +58,10 @@ message MsgExecute {
string target = 2;
// message is the message to be sent to the account.
google.protobuf.Any message = 3;
// funds contains the coins that the account wants to
// send alongside the request.
repeated cosmos.base.v1beta1.Coin funds = 4
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// MsgExecuteResponse defines the Execute response type for the Msg/Execute RPC method.
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/accounts/account_abstraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ func TestAccountAbstraction(t *testing.T) {

_, aaAddr, err := ak.Init(ctx, "aa_minimal", accCreator, &rotationv1.MsgInit{
PubKeyBytes: privKey.PubKey().Bytes(),
})
}, nil)
require.NoError(t, err)

_, aaFullAddr, err := ak.Init(ctx, "aa_full", accCreator, &rotationv1.MsgInit{
PubKeyBytes: privKey.PubKey().Bytes(),
})
}, nil)
require.NoError(t, err)

aaAddrStr, err := app.AuthKeeper.AddressCodec().BytesToString(aaAddr)
Expand Down
27 changes: 24 additions & 3 deletions tests/e2e/accounts/wiring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"testing"

"cosmossdk.io/core/header"
storetypes "cosmossdk.io/store/types"
counterv1 "cosmossdk.io/x/accounts/testing/counter/v1"
"cosmossdk.io/x/bank/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
Expand All @@ -17,24 +19,36 @@ import (
// - binary codec
// - header service
// - gas service
// - funds
func TestDependencies(t *testing.T) {
app := setupApp(t)
ak := app.AccountsKeeper
ctx := sdk.NewContext(app.CommitMultiStore(), false, app.Logger()).WithHeaderInfo(header.Info{ChainID: "chain-id"})
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(500_000))

_, counterAddr, err := ak.Init(ctx, "counter", accCreator, &counterv1.MsgInit{
InitialValue: 0,
})
}, nil)
require.NoError(t, err)
// test dependencies
r, err := ak.Execute(ctx, counterAddr, []byte("test"), &counterv1.MsgTestDependencies{})
creatorInitFunds := sdk.NewCoins(sdk.NewInt64Coin("stake", 100_000))
err = testutil.FundAccount(ctx, app.BankKeeper, accCreator, creatorInitFunds)
require.NoError(t, err)
sentFunds := sdk.NewCoins(sdk.NewInt64Coin("stake", 50_000))
r, err := ak.Execute(
ctx,
counterAddr,
accCreator,
&counterv1.MsgTestDependencies{},
sentFunds,
)
require.NoError(t, err)
res := r.(*counterv1.MsgTestDependenciesResponse)

// test gas
require.NotZero(t, res.BeforeGas)
require.NotZero(t, res.AfterGas)
require.Equal(t, uint64(10), res.AfterGas-res.BeforeGas)
require.Equal(t, int(uint64(10)), int(res.AfterGas-res.BeforeGas))

// test header service
require.Equal(t, ctx.HeaderInfo().ChainID, res.ChainId)
Expand All @@ -43,4 +57,11 @@ func TestDependencies(t *testing.T) {
wantAddr, err := app.AuthKeeper.AddressCodec().BytesToString(counterAddr)
require.NoError(t, err)
require.Equal(t, wantAddr, res.Address)

// test funds
creatorFunds := app.BankKeeper.GetAllBalances(ctx, accCreator)
require.Equal(t, creatorInitFunds.Sub(sentFunds...), creatorFunds)

accFunds := app.BankKeeper.GetAllBalances(ctx, counterAddr)
require.Equal(t, sentFunds, accFunds)
}
5 changes: 5 additions & 0 deletions x/accounts/accountstd/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"cosmossdk.io/x/accounts/internal/implementation"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
)

Expand Down Expand Up @@ -77,6 +78,10 @@ func SenderIsAccountsModule(ctx context.Context) bool {
return bytes.Equal(Sender(ctx), accountsModuleAddress)
}

// Funds returns if any funds were sent during the execute or init request. In queries this
// returns nil.
func Funds(ctx context.Context) sdk.Coins { return implementation.Funds(ctx) }

// ExecModule can be used to execute a message towards a module.
func ExecModule[Resp any, RespProto implementation.ProtoMsgG[Resp], Req any, ReqProto implementation.ProtoMsgG[Req]](ctx context.Context, msg ReqProto) (RespProto, error) {
return implementation.ExecModule[Resp, RespProto, Req, ReqProto](ctx, msg)
Expand Down
52 changes: 52 additions & 0 deletions x/accounts/coin_transfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package accounts

import (
"google.golang.org/protobuf/proto"

bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
"cosmossdk.io/core/address"
"cosmossdk.io/x/accounts/internal/implementation"

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

// protoV2GogoWrapper is a wrapper of a protov2 message into a gogo message.
// this is exceptionally allowed to enable accounts to be decoupled from
// the SDK, since x/accounts can support only protov1 in its APIs.
// But in order to keep it decoupled from the SDK we need to use the API module.
// This is a hack to make an API module type work in x/accounts. Once the SDK
// has protov2 support, we can migrate internal/implementation/encoding.go to
// work with protov2.
type protoV2GogoWrapper struct {
gogoProtoPlusV2
}

func (h protoV2GogoWrapper) XXX_MessageName() string {
return string(proto.MessageName(h.gogoProtoPlusV2))
}

func defaultCoinsTransferMsgFunc(addrCdc address.Codec) coinsTransferMsgFunc {
return func(from, to []byte, coins sdk.Coins) (implementation.ProtoMsg, implementation.ProtoMsg, error) {
fromAddr, err := addrCdc.BytesToString(from)
if err != nil {
return nil, nil, err
}
toAddr, err := addrCdc.BytesToString(to)
if err != nil {
return nil, nil, err
}
v2Coins := make([]*v1beta1.Coin, len(coins))
for i, coin := range coins {
v2Coins[i] = &v1beta1.Coin{
Denom: coin.Denom,
Amount: coin.Amount.String(),
}
}
return protoV2GogoWrapper{&bankv1beta1.MsgSend{
FromAddress: fromAddr,
ToAddress: toAddr,
Amount: v2Coins,
}}, new(bankv1beta1.MsgSendResponse), nil
}
}
8 changes: 4 additions & 4 deletions x/accounts/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ func TestGenesis(t *testing.T) {
// we init two accounts of the same type

// we set counter to 10
_, addr1, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{})
_, addr1, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{}, nil)
require.NoError(t, err)
_, err = k.Execute(ctx, addr1, []byte("sender"), &types.UInt64Value{Value: 10})
_, err = k.Execute(ctx, addr1, []byte("sender"), &types.UInt64Value{Value: 10}, nil)
require.NoError(t, err)

// we set counter to 20
_, addr2, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{})
_, addr2, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{}, nil)
require.NoError(t, err)
_, err = k.Execute(ctx, addr2, []byte("sender"), &types.UInt64Value{Value: 20})
_, err = k.Execute(ctx, addr2, []byte("sender"), &types.UInt64Value{Value: 20}, nil)
require.NoError(t, err)

// export state
Expand Down
46 changes: 28 additions & 18 deletions x/accounts/internal/implementation/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"cosmossdk.io/core/header"
"cosmossdk.io/core/store"
"cosmossdk.io/x/accounts/internal/prefixstore"

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

var AccountStatePrefix = collections.NewPrefix(255)
Expand All @@ -25,12 +27,21 @@ type contextValue struct {
store store.KVStore // store is the prefixed store for the account.
sender []byte // sender is the address of the entity invoking the account action.
whoami []byte // whoami is the address of the account being invoked.
funds sdk.Coins // funds reports the coins sent alongside the request.
parentContext context.Context // parentContext that was used to build the account context.
moduleExec ModuleExecFunc // moduleExec is a function that executes a module message, when the resp type is known.
moduleExecUntyped ModuleExecUntypedFunc // moduleExecUntyped is a function that executes a module message, when the resp type is unknown.
moduleQuery ModuleQueryFunc // moduleQuery is a function that queries a module.
}

func addCtx(ctx context.Context, value contextValue) context.Context {
return context.WithValue(ctx, contextKey{}, value)
}

func getCtx(ctx context.Context) contextValue {
return ctx.Value(contextKey{}).(contextValue)
}

// MakeAccountContext creates a new account execution context given:
// storeSvc: which fetches the x/accounts module store.
// accountAddr: the address of the account being invoked, which is used to give the
Expand All @@ -44,14 +55,16 @@ func MakeAccountContext(
accNumber uint64,
accountAddr []byte,
sender []byte,
funds sdk.Coins,
moduleExec ModuleExecFunc,
moduleExecUntyped ModuleExecUntypedFunc,
moduleQuery ModuleQueryFunc,
) context.Context {
return context.WithValue(ctx, contextKey{}, contextValue{
return addCtx(ctx, contextValue{
store: makeAccountStore(ctx, storeSvc, accNumber),
sender: sender,
whoami: accountAddr,
funds: funds,
parentContext: ctx,
moduleExec: moduleExec,
moduleExecUntyped: moduleExecUntyped,
Expand All @@ -71,7 +84,7 @@ func makeAccountStore(ctx context.Context, storeSvc store.KVStoreService, accNum
// ExecModuleUntyped can be used to execute a message towards a module, when the response type is unknown.
func ExecModuleUntyped(ctx context.Context, msg ProtoMsg) (ProtoMsg, error) {
// get sender
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)

resp, err := v.moduleExecUntyped(v.parentContext, v.whoami, msg)
if err != nil {
Expand All @@ -84,7 +97,7 @@ func ExecModuleUntyped(ctx context.Context, msg ProtoMsg) (ProtoMsg, error) {
// ExecModule can be used to execute a message towards a module.
func ExecModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG[Req]](ctx context.Context, msg ReqProto) (RespProto, error) {
// get sender
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)

// execute module, unwrapping the original context.
resp := RespProto(new(Resp))
Expand All @@ -100,7 +113,7 @@ func ExecModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG
func QueryModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG[Req]](ctx context.Context, req ReqProto) (RespProto, error) {
// we do not need to check the sender in a query because it is not a state transition.
// we also unwrap the original context.
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
resp := RespProto(new(Resp))
err := v.moduleQuery(v.parentContext, req, resp)
if err != nil {
Expand All @@ -110,20 +123,21 @@ func QueryModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsg
}

// openKVStore returns the prefixed store for the account given the context.
func openKVStore(ctx context.Context) store.KVStore {
return ctx.Value(contextKey{}).(contextValue).store
}
func openKVStore(ctx context.Context) store.KVStore { return getCtx(ctx).store }

// Sender returns the address of the entity invoking the account action.
func Sender(ctx context.Context) []byte {
return ctx.Value(contextKey{}).(contextValue).sender
return getCtx(ctx).sender
}

// Whoami returns the address of the account being invoked.
func Whoami(ctx context.Context) []byte {
return ctx.Value(contextKey{}).(contextValue).whoami
return getCtx(ctx).whoami
}

// Funds returns the funds associated with the execution context.
func Funds(ctx context.Context) sdk.Coins { return getCtx(ctx).funds }

type headerService struct{ hs header.Service }

func (h headerService) GetHeaderInfo(ctx context.Context) header.Info {
Expand All @@ -132,9 +146,7 @@ func (h headerService) GetHeaderInfo(ctx context.Context) header.Info {

var _ gas.Service = (*gasService)(nil)

type gasService struct {
gs gas.Service
}
type gasService struct{ gs gas.Service }

func (g gasService) GetGasMeter(ctx context.Context) gas.Meter {
return g.gs.GetGasMeter(getParentContext(ctx))
Expand All @@ -145,17 +157,15 @@ func (g gasService) GetBlockGasMeter(ctx context.Context) gas.Meter {
}

func (g gasService) WithGasMeter(ctx context.Context, meter gas.Meter) context.Context {
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
v.parentContext = g.gs.WithGasMeter(v.parentContext, meter)
return context.WithValue(v.parentContext, contextKey{}, v)
}

func (g gasService) WithBlockGasMeter(ctx context.Context, meter gas.Meter) context.Context {
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
v.parentContext = g.gs.WithBlockGasMeter(v.parentContext, meter)
return context.WithValue(v.parentContext, contextKey{}, v)
return addCtx(v.parentContext, v)
}

func getParentContext(ctx context.Context) context.Context {
return ctx.Value(contextKey{}).(contextValue).parentContext
}
func getParentContext(ctx context.Context) context.Context { return getCtx(ctx).parentContext }
Loading

0 comments on commit 5be33f5

Please sign in to comment.