feat(accounts): add Account interface and Implementation (#17495)

Co-authored-by: unknown unknown <unknown@unknown>
This commit is contained in:
testinginprod
2023-08-22 18:17:34 +00:00
committed by GitHub
co-authored by unknown unknown
parent 3781c462a9
commit 780cad872b
9 changed files with 379 additions and 1 deletions
+8 -1
View File
@@ -4,16 +4,23 @@ go 1.21
require (
github.com/cosmos/gogoproto v1.4.11
github.com/stretchr/testify v1.8.4
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// REMOVE post v1 release.
replace cosmossdk.io/api => ../../api
+10
View File
@@ -1,11 +1,17 @@
github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g=
github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
@@ -23,3 +29,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,36 @@
package implementation
import (
"context"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var _ Account = (*TestAccount)(nil)
type TestAccount struct{}
func (TestAccount) RegisterInitHandler(builder *InitBuilder) {
RegisterInitHandler(builder, func(_ context.Context, req *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
return &wrapperspb.StringValue{Value: req.Value + "init-echo"}, nil
})
}
func (TestAccount) RegisterExecuteHandlers(builder *ExecuteBuilder) {
RegisterExecuteHandler(builder, func(_ context.Context, req *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
return &wrapperspb.StringValue{Value: req.Value + "execute-echo"}, nil
})
RegisterExecuteHandler(builder, func(_ context.Context, req *wrapperspb.BytesValue) (*wrapperspb.BytesValue, error) {
return &wrapperspb.BytesValue{Value: append(req.Value, "bytes-execute-echo"...)}, nil
})
}
func (TestAccount) RegisterQueryHandlers(builder *QueryBuilder) {
RegisterQueryHandler(builder, func(_ context.Context, req *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
return &wrapperspb.StringValue{Value: req.Value + "query-echo"}, nil
})
RegisterQueryHandler(builder, func(_ context.Context, req *wrapperspb.BytesValue) (*wrapperspb.BytesValue, error) {
return &wrapperspb.BytesValue{Value: append(req.Value, "bytes-query-echo"...)}, nil
})
}
@@ -0,0 +1,106 @@
package implementation
import (
"context"
"errors"
"fmt"
"google.golang.org/protobuf/reflect/protoreflect"
)
var (
errNoInitHandler = errors.New("no init handler")
errNoExecuteHandler = errors.New("account does not accept messages")
errInvalidMessage = errors.New("invalid message")
)
// NewInitBuilder creates a new InitBuilder instance.
func NewInitBuilder() *InitBuilder {
return &InitBuilder{}
}
// InitBuilder defines a smart account's initialisation handler builder.
type InitBuilder struct {
// handler is the handler function that will be called when the smart account is initialized.
// Although the function here is defined to take an any, the smart account will work
// with a typed version of it.
handler func(ctx context.Context, initRequest any) (initResponse any, err error)
}
// makeHandler returns the handler function that will be called when the smart account is initialized.
// It returns an error if no handler was registered.
func (i *InitBuilder) makeHandler() (func(ctx context.Context, initRequest any) (initResponse any, err error), error) {
if i.handler == nil {
return nil, errNoInitHandler
}
return i.handler, nil
}
// NewExecuteBuilder creates a new ExecuteBuilder instance.
func NewExecuteBuilder() *ExecuteBuilder {
return &ExecuteBuilder{
handlers: make(map[string]func(ctx context.Context, executeRequest any) (executeResponse any, err error)),
}
}
// ExecuteBuilder defines a smart account's execution router, it will be used to map an execution message
// to a handler function for a specific account.
type ExecuteBuilder struct {
// handlers is a map of handler functions that will be called when the smart account is executed.
handlers map[string]func(ctx context.Context, executeRequest any) (executeResponse any, err error)
// err is the error that occurred before building the handler function.
err error
}
func (r *ExecuteBuilder) getMessageName(msg any) (string, error) {
protoMsg, ok := msg.(protoreflect.ProtoMessage)
if !ok {
return "", fmt.Errorf("%w: expected protoreflect.Message, got %T", errInvalidMessage, msg)
}
return string(protoMsg.ProtoReflect().Descriptor().FullName()), nil
}
func (r *ExecuteBuilder) makeHandler() (func(ctx context.Context, executeRequest any) (executeResponse any, err error), error) {
// if no handler is registered it's fine, it means the account will not be accepting execution or query messages.
if len(r.handlers) == 0 {
return func(ctx context.Context, _ any) (_ any, err error) {
return nil, errNoExecuteHandler
}, nil
}
if r.err != nil {
return nil, r.err
}
// build the real execution handler
return func(ctx context.Context, executeRequest any) (executeResponse any, err error) {
messageName, err := r.getMessageName(executeRequest)
if err != nil {
return nil, fmt.Errorf("%w: unable to get message name", err)
}
handler, ok := r.handlers[messageName]
if !ok {
return nil, fmt.Errorf("%w: no handler for message %s", errInvalidMessage, messageName)
}
return handler(ctx, executeRequest)
}, nil
}
// NewQueryBuilder creates a new QueryBuilder instance.
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{
er: NewExecuteBuilder(),
}
}
// QueryBuilder defines a smart account's query router, it will be used to map a query message
// to a handler function for a specific account.
type QueryBuilder struct {
// er is the ExecuteBuilder, since there's no difference between the execution and query handlers API.
er *ExecuteBuilder
}
func (r *QueryBuilder) makeHandler() (func(ctx context.Context, queryRequest any) (queryResponse any, err error), error) {
return r.er.makeHandler()
}
@@ -0,0 +1,35 @@
package implementation
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func TestRouterDoubleRegistration(t *testing.T) {
router := NewExecuteBuilder()
RegisterExecuteHandler(router, func(_ context.Context, req *wrapperspb.StringValue) (*wrapperspb.StringValue, error) { return nil, nil })
RegisterExecuteHandler(router, func(_ context.Context, req *wrapperspb.StringValue) (*wrapperspb.StringValue, error) { return nil, nil })
_, err := router.makeHandler()
require.ErrorContains(t, err, "already registered")
}
func TestEmptyQueryExecuteHandler(t *testing.T) {
qr := NewQueryBuilder()
er := NewExecuteBuilder()
qh, err := qr.makeHandler()
require.NoError(t, err)
eh, err := er.makeHandler()
require.NoError(t, err)
ctx := context.Background()
_, err = qh(ctx, &wrapperspb.StringValue{})
require.ErrorIs(t, err, errNoExecuteHandler)
_, err = eh(ctx, &wrapperspb.StringValue{})
require.ErrorIs(t, err, errNoExecuteHandler)
}
@@ -0,0 +1,46 @@
package implementation
import "context"
// NewImplementation creates a new Implementation instance given an Account implementer.
func NewImplementation(account Account) (Implementation, error) {
// make init handler
ir := NewInitBuilder()
account.RegisterInitHandler(ir)
initHandler, err := ir.makeHandler()
if err != nil {
return Implementation{}, err
}
// make execute handler
er := NewExecuteBuilder()
account.RegisterExecuteHandlers(er)
executeHandler, err := er.makeHandler()
if err != nil {
return Implementation{}, err
}
// make query handler
qr := NewQueryBuilder()
account.RegisterQueryHandlers(qr)
queryHandler, err := qr.makeHandler()
if err != nil {
return Implementation{}, err
}
return Implementation{
Init: initHandler,
Execute: executeHandler,
Query: queryHandler,
}, nil
}
// Implementation wraps an Account implementer in order to provide a concrete
// and non-generic implementation usable by the x/accounts module.
type Implementation struct {
// Init defines the initialisation handler for the smart account.
Init func(ctx context.Context, msg any) (resp any, err error)
// Execute defines the execution handler for the smart account.
Execute func(ctx context.Context, msg any) (resp any, err error)
// Query defines the query handler for the smart account.
Query func(ctx context.Context, msg any) (resp any, err error)
}
@@ -0,0 +1,66 @@
package implementation
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func TestImplementation(t *testing.T) {
impl, err := NewImplementation(TestAccount{})
require.NoError(t, err)
ctx := context.Background()
t.Run("execute ok", func(t *testing.T) {
resp, err := impl.Execute(ctx, &wrapperspb.StringValue{Value: "test"})
require.NoError(t, err)
require.Equal(t, "testexecute-echo", resp.(*wrapperspb.StringValue).Value)
resp, err = impl.Execute(ctx, &wrapperspb.BytesValue{Value: []byte("test")})
require.NoError(t, err)
require.Equal(t, "testbytes-execute-echo", string(resp.(*wrapperspb.BytesValue).Value))
})
t.Run("execute - unknown message", func(t *testing.T) {
_, err := impl.Execute(ctx, &wrapperspb.Int32Value{Value: 1})
require.ErrorIs(t, err, errInvalidMessage)
})
t.Run("init ok", func(t *testing.T) {
resp, err := impl.Init(ctx, &wrapperspb.StringValue{Value: "test"})
require.NoError(t, err)
require.Equal(t, "testinit-echo", resp.(*wrapperspb.StringValue).Value)
})
t.Run("init - unknown message", func(t *testing.T) {
_, err := impl.Init(ctx, &wrapperspb.Int32Value{Value: 1})
require.ErrorIs(t, err, errInvalidMessage)
})
t.Run("query ok", func(t *testing.T) {
resp, err := impl.Query(ctx, &wrapperspb.StringValue{Value: "test"})
require.NoError(t, err)
require.Equal(t, "testquery-echo", resp.(*wrapperspb.StringValue).Value)
resp, err = impl.Query(ctx, &wrapperspb.BytesValue{Value: []byte("test")})
require.NoError(t, err)
require.Equal(t, "testbytes-query-echo", string(resp.(*wrapperspb.BytesValue).Value))
})
t.Run("query - unknown message", func(t *testing.T) {
_, err := impl.Query(ctx, &wrapperspb.Int32Value{Value: 1})
require.ErrorIs(t, err, errInvalidMessage)
})
t.Run("all - not a protobuf message", func(t *testing.T) {
_, err := impl.Execute(ctx, "test")
require.ErrorIs(t, err, errInvalidMessage)
_, err = impl.Query(ctx, "test")
require.ErrorIs(t, err, errInvalidMessage)
_, err = impl.Init(ctx, "test")
require.ErrorIs(t, err, errInvalidMessage)
})
}
@@ -0,0 +1,17 @@
package implementation
// Account defines a smart account interface.
type Account interface {
// RegisterInitHandler allows the smart account to register an initialisation handler, using
// the provided InitBuilder. The handler will be called when the smart account is initialized
// (deployed).
RegisterInitHandler(builder *InitBuilder)
// RegisterExecuteHandlers allows the smart account to register execution handlers.
// The smart account might also decide to not register any execution handler.
RegisterExecuteHandlers(builder *ExecuteBuilder)
// RegisterQueryHandlers allows the smart account to register query handlers. The smart account
// might also decide to not register any query handler.
RegisterQueryHandlers(builder *QueryBuilder)
}
@@ -0,0 +1,55 @@
package implementation
import (
"context"
"fmt"
"google.golang.org/protobuf/reflect/protoreflect"
)
// ProtoMsg is a generic interface for protobuf messages.
type ProtoMsg[T any] interface {
*T
protoreflect.ProtoMessage
}
// RegisterInitHandler registers an initialisation handler for a smart account that uses protobuf.
func RegisterInitHandler[
Req any, ProtoReq ProtoMsg[Req], Resp any, ProtoResp ProtoMsg[Resp],
](router *InitBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error)) {
reqName := ProtoReq(new(Req)).ProtoReflect().Descriptor().FullName()
router.handler = func(ctx context.Context, initRequest any) (initResponse any, err error) {
concrete, ok := initRequest.(ProtoReq)
if !ok {
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, initRequest)
}
return handler(ctx, concrete)
}
}
// RegisterExecuteHandler registers an execution handler for a smart account that uses protobuf.
func RegisterExecuteHandler[
Req any, ProtoReq ProtoMsg[Req], Resp any, ProtoResp ProtoMsg[Resp],
](router *ExecuteBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error)) {
reqName := ProtoReq(new(Req)).ProtoReflect().Descriptor().FullName()
// check if not registered already
if _, ok := router.handlers[string(reqName)]; ok {
router.err = fmt.Errorf("handler already registered for message %s", reqName)
return
}
router.handlers[string(reqName)] = func(ctx context.Context, executeRequest any) (executeResponse any, err error) {
concrete, ok := executeRequest.(ProtoReq)
if !ok {
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, executeRequest)
}
return handler(ctx, concrete)
}
}
// RegisterQueryHandler registers a query handler for a smart account that uses protobuf.
func RegisterQueryHandler[
Req any, ProtoReq ProtoMsg[Req], Resp any, ProtoResp ProtoMsg[Resp],
](router *QueryBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error)) {
RegisterExecuteHandler(router.er, handler)
}