cosmos-sdk/x/accounts/internal/implementation/api_builder.go
testinginprod c60283b91a
feat(accounts): Add message server and query server logic. (#17614)
Co-authored-by: unknown unknown <unknown@unknown>
Co-authored-by: samricotta <37125168+samricotta@users.noreply.github.com>
2023-09-07 07:42:23 +00:00

150 lines
5.0 KiB
Go

package implementation
import (
"context"
"errors"
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
)
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)
// decodeRequest is the function that will be used to decode the init request from bytes.
decodeRequest func([]byte) (any, error)
// encodeResponse is the function that will be used to encode the init response to bytes.
encodeResponse func(any) ([]byte, 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
}
func (r *ExecuteBuilder) makeRequestDecoder() func(requestBytes []byte) (any, error) {
return func(requestBytes []byte) (any, error) {
anyPB := new(anypb.Any)
err := proto.Unmarshal(requestBytes, anyPB)
if err != nil {
return nil, err
}
msg, err := anyPB.UnmarshalNew()
if err != nil {
return nil, err
}
// we do not check if it is part of a valid message set as an account can handle
// and the handler will do so.
return msg, nil
}
}
func (r *ExecuteBuilder) makeResponseEncoder() func(executeResponse any) ([]byte, error) {
return func(executeResponse any) ([]byte, error) {
executeResponsePB, ok := executeResponse.(protoreflect.ProtoMessage)
if !ok {
return nil, fmt.Errorf("%w: expected protoreflect.Message, got %T", errInvalidMessage, executeResponse)
}
anyPB, err := anypb.New(executeResponsePB)
if err != nil {
return nil, err
}
// we do not check if it is part of an account's valid response message set
// as make handler will never allow for an invalid response to be returned.
return proto.Marshal(anyPB)
}
}
// 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()
}