feat(x/tx): Support gogo registry in Textual (#15302)

This commit is contained in:
Amaury 2023-03-20 09:13:27 +01:00 committed by GitHub
parent bf85bfbf4d
commit 124aed4b28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 301 additions and 109 deletions

2
go.mod
View File

@ -11,7 +11,7 @@ require (
cosmossdk.io/log v0.1.0
cosmossdk.io/math v1.0.0-rc.0
cosmossdk.io/store v0.1.0-alpha.1
cosmossdk.io/x/tx v0.3.0
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f
github.com/99designs/keyring v1.2.1
github.com/armon/go-metrics v0.4.1
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816

4
go.sum
View File

@ -51,8 +51,8 @@ cosmossdk.io/math v1.0.0-rc.0 h1:ml46ukocrAAoBpYKMidF0R2tQJ1Uxfns0yH8wqgMAFc=
cosmossdk.io/math v1.0.0-rc.0/go.mod h1:Ygz4wBHrgc7g0N+8+MrnTfS9LLn9aaTGa9hKopuym5k=
cosmossdk.io/store v0.1.0-alpha.1 h1:NGomhLUXzAxvK4OF8+yP6eNUG5i4LwzOzx+S494pTCg=
cosmossdk.io/store v0.1.0-alpha.1/go.mod h1:kmCMbhrleCZ6rDZPY/EGNldNvPebFNyVPFYp+pv05/k=
cosmossdk.io/x/tx v0.3.0 h1:AgVYy6bxL3XqEV7RLyxFh1uT+wywsrbgVMmYnL3FgWM=
cosmossdk.io/x/tx v0.3.0/go.mod h1:ELY0bP2SmOqyffJFp00g979xsI1zBdmc55A6JCi1Qe8=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f h1:yXEE3D6L0Ykwlp4FuS1SoHgT9vZ8brBJ/dkHezXBU9o=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f/go.mod h1:V/7DjCSReJ7LBBYrNtVFUec7t63hVNyFh0vBXOBK2Yg=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=

View File

@ -38,7 +38,7 @@ require (
cloud.google.com/go/storage v1.29.0 // indirect
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba // indirect
cosmossdk.io/errors v1.0.0-beta.7 // indirect
cosmossdk.io/x/tx v0.3.0 // indirect
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect

View File

@ -206,8 +206,8 @@ cosmossdk.io/math v1.0.0-rc.0 h1:ml46ukocrAAoBpYKMidF0R2tQJ1Uxfns0yH8wqgMAFc=
cosmossdk.io/math v1.0.0-rc.0/go.mod h1:Ygz4wBHrgc7g0N+8+MrnTfS9LLn9aaTGa9hKopuym5k=
cosmossdk.io/store v0.1.0-alpha.1 h1:NGomhLUXzAxvK4OF8+yP6eNUG5i4LwzOzx+S494pTCg=
cosmossdk.io/store v0.1.0-alpha.1/go.mod h1:kmCMbhrleCZ6rDZPY/EGNldNvPebFNyVPFYp+pv05/k=
cosmossdk.io/x/tx v0.3.0 h1:AgVYy6bxL3XqEV7RLyxFh1uT+wywsrbgVMmYnL3FgWM=
cosmossdk.io/x/tx v0.3.0/go.mod h1:ELY0bP2SmOqyffJFp00g979xsI1zBdmc55A6JCi1Qe8=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f h1:yXEE3D6L0Ykwlp4FuS1SoHgT9vZ8brBJ/dkHezXBU9o=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f/go.mod h1:V/7DjCSReJ7LBBYrNtVFUec7t63hVNyFh0vBXOBK2Yg=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=

View File

@ -85,10 +85,14 @@ func NewRootCmd() *cobra.Command {
// TODO Currently, the TxConfig below doesn't include Textual, so
// an error will arise when using the --textual flag.
// ref: https://github.com/cosmos/cosmos-sdk/issues/11970
txt, err := txmodule.NewTextualWithGRPCConn(initClientCtx)
if err != nil {
return err
}
txConfigWithTextual := tx.NewTxConfigWithTextual(
codec.NewProtoCodec(encodingConfig.InterfaceRegistry),
encodingConfig.TxConfig.SignModeHandler().Modes(),
txmodule.NewTextualWithGRPCConn(initClientCtx),
txt,
)
initClientCtx = initClientCtx.WithTxConfig(txConfigWithTextual)

View File

@ -13,7 +13,7 @@ require (
cosmossdk.io/x/evidence v0.1.0
cosmossdk.io/x/feegrant v0.0.0-20230117113717-50e7c4a4ceff
cosmossdk.io/x/nft v0.0.0-20230113085233-fae3332d62fc
cosmossdk.io/x/tx v0.3.0
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f
cosmossdk.io/x/upgrade v0.0.0-20230127052425-54c8e1568335
github.com/cometbft/cometbft v0.37.0
github.com/cosmos/cosmos-db v1.0.0-rc.1

View File

@ -206,8 +206,8 @@ cosmossdk.io/math v1.0.0-rc.0 h1:ml46ukocrAAoBpYKMidF0R2tQJ1Uxfns0yH8wqgMAFc=
cosmossdk.io/math v1.0.0-rc.0/go.mod h1:Ygz4wBHrgc7g0N+8+MrnTfS9LLn9aaTGa9hKopuym5k=
cosmossdk.io/store v0.1.0-alpha.1 h1:NGomhLUXzAxvK4OF8+yP6eNUG5i4LwzOzx+S494pTCg=
cosmossdk.io/store v0.1.0-alpha.1/go.mod h1:kmCMbhrleCZ6rDZPY/EGNldNvPebFNyVPFYp+pv05/k=
cosmossdk.io/x/tx v0.3.0 h1:AgVYy6bxL3XqEV7RLyxFh1uT+wywsrbgVMmYnL3FgWM=
cosmossdk.io/x/tx v0.3.0/go.mod h1:ELY0bP2SmOqyffJFp00g979xsI1zBdmc55A6JCi1Qe8=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f h1:yXEE3D6L0Ykwlp4FuS1SoHgT9vZ8brBJ/dkHezXBU9o=
cosmossdk.io/x/tx v0.3.1-0.20230320072322-5fceb7c0495f/go.mod h1:V/7DjCSReJ7LBBYrNtVFUec7t63hVNyFh0vBXOBK2Yg=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=

View File

@ -131,10 +131,12 @@ func TestSigVerification(t *testing.T) {
enabledSignModes := []signing.SignMode{signing.SignMode_SIGN_MODE_DIRECT, signing.SignMode_SIGN_MODE_TEXTUAL, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON}
// Since TEXTUAL is not enabled by default, we create a custom TxConfig
// here which includes it.
txt, err := txmodule.NewTextualWithGRPCConn(suite.clientCtx)
require.NoError(t, err)
suite.clientCtx.TxConfig = authtx.NewTxConfigWithTextual(
codec.NewProtoCodec(suite.encCfg.InterfaceRegistry),
enabledSignModes,
txmodule.NewTextualWithGRPCConn(suite.clientCtx),
txt,
)
suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder()
@ -161,10 +163,12 @@ func TestSigVerification(t *testing.T) {
gasLimit := testdata.NewTestGasLimit()
spkd := ante.NewSetPubKeyDecorator(suite.accountKeeper)
txt, err = txmodule.NewTextualWithBankKeeper(suite.txBankKeeper)
require.NoError(t, err)
anteTxConfig := authtx.NewTxConfigWithTextual(
codec.NewProtoCodec(suite.encCfg.InterfaceRegistry),
enabledSignModes,
txmodule.NewTextualWithBankKeeper(suite.txBankKeeper),
txt,
)
svd := ante.NewSigVerificationDecorator(suite.accountKeeper, anteTxConfig.SignModeHandler())
antehandler := sdk.ChainAnteDecorators(spkd, svd)

View File

@ -50,7 +50,10 @@ type TxOutputs struct {
}
func ProvideModule(in TxInputs) TxOutputs {
textual := NewTextualWithBankKeeper(in.TxBankKeeper)
textual, err := NewTextualWithBankKeeper(in.TxBankKeeper)
if err != nil {
panic(err)
}
var txConfig client.TxConfig
if in.CustomSignModeHandlers == nil {
txConfig = tx.NewTxConfigWithTextual(in.ProtoCodecMarshaler, tx.DefaultSignModes, textual)

View File

@ -3,6 +3,7 @@ package tx
import (
"context"
gogoproto "github.com/cosmos/gogoproto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
@ -21,17 +22,25 @@ import (
//
// clientCtx := client.GetClientContextFromCmd(cmd)
// txt := tx.NewTextualWithGRPCConn(clientCtxx)
func NewTextualWithGRPCConn(grpcConn grpc.ClientConnInterface) *textual.SignModeHandler {
return textual.NewSignModeHandler(func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) {
bankQueryClient := bankv1beta1.NewQueryClient(grpcConn)
res, err := bankQueryClient.DenomMetadata(ctx, &bankv1beta1.QueryDenomMetadataRequest{
Denom: denom,
})
if err != nil {
return nil, metadataExists(err)
}
func NewTextualWithGRPCConn(grpcConn grpc.ClientConnInterface) (*textual.SignModeHandler, error) {
protoFiles, err := gogoproto.MergedRegistry()
if err != nil {
return nil, err
}
return res.Metadata, nil
return textual.NewSignModeHandler(textual.SignModeOptions{
CoinMetadataQuerier: func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) {
bankQueryClient := bankv1beta1.NewQueryClient(grpcConn)
res, err := bankQueryClient.DenomMetadata(ctx, &bankv1beta1.QueryDenomMetadataRequest{
Denom: denom,
})
if err != nil {
return nil, metadataExists(err)
}
return res.Metadata, nil
},
FileResolver: protoFiles,
})
}
@ -41,37 +50,43 @@ func NewTextualWithGRPCConn(grpcConn grpc.ClientConnInterface) *textual.SignMode
// Note: Once we switch to ADR-033, and keepers become ADR-033 clients to each
// other, this function could probably be deprecated in favor of
// `NewTextualWithGRPCConn`.
func NewTextualWithBankKeeper(bk BankKeeper) *textual.SignModeHandler {
textual := textual.NewSignModeHandler(func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) {
res, err := bk.DenomMetadata(ctx, &types.QueryDenomMetadataRequest{Denom: denom})
if err != nil {
return nil, metadataExists(err)
}
func NewTextualWithBankKeeper(bk BankKeeper) (*textual.SignModeHandler, error) {
protoFiles, err := gogoproto.MergedRegistry()
if err != nil {
return nil, err
}
m := &bankv1beta1.Metadata{
Base: res.Metadata.Base,
Display: res.Metadata.Display,
// fields below are not strictly needed by Textual
// but added here for completeness.
Description: res.Metadata.Description,
Name: res.Metadata.Name,
Symbol: res.Metadata.Symbol,
Uri: res.Metadata.URI,
UriHash: res.Metadata.URIHash,
}
m.DenomUnits = make([]*bankv1beta1.DenomUnit, len(res.Metadata.DenomUnits))
for i, d := range res.Metadata.DenomUnits {
m.DenomUnits[i] = &bankv1beta1.DenomUnit{
Denom: d.Denom,
Exponent: d.Exponent,
Aliases: d.Aliases,
return textual.NewSignModeHandler(textual.SignModeOptions{
CoinMetadataQuerier: func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) {
res, err := bk.DenomMetadata(ctx, &types.QueryDenomMetadataRequest{Denom: denom})
if err != nil {
return nil, metadataExists(err)
}
}
return m, nil
m := &bankv1beta1.Metadata{
Base: res.Metadata.Base,
Display: res.Metadata.Display,
// fields below are not strictly needed by Textual
// but added here for completeness.
Description: res.Metadata.Description,
Name: res.Metadata.Name,
Symbol: res.Metadata.Symbol,
Uri: res.Metadata.URI,
UriHash: res.Metadata.URIHash,
}
m.DenomUnits = make([]*bankv1beta1.DenomUnit, len(res.Metadata.DenomUnits))
for i, d := range res.Metadata.DenomUnits {
m.DenomUnits[i] = &bankv1beta1.DenomUnit{
Denom: d.Denom,
Exponent: d.Exponent,
Aliases: d.Aliases,
}
}
return m, nil
},
FileResolver: protoFiles,
})
return textual
}
// metadataExists parses the error, and only propagates the error if it's

View File

@ -34,3 +34,9 @@ Ref: https://keepachangelog.com/en/1.0.0/
## API Breaking
* [#15278](https://github.com/cosmos/cosmos-sdk/pull/15278) Move `x/tx/{textual,aminojson}` into `x/tx/signing`.
* [#15302](https://github.com/cosmos/cosmos-sdk/pull/15302) `textual.NewSignModeHandler` now takes an options struct instead of a simple coin querier argument. It also returns an error.
## Improvements
* [#15302](https://github.com/cosmos/cosmos-sdk/pull/15302) Add support for a custom registry (e.g. gogo's MergedRegistry) to be plugged into SIGN_MODE_TEXTUAL.

View File

@ -1,8 +1,6 @@
package std
import (
"fmt"
"cosmossdk.io/x/tx/signing"
"cosmossdk.io/x/tx/signing/direct"
"cosmossdk.io/x/tx/signing/textual"
@ -10,19 +8,20 @@ import (
// SignModeOptions are options for configuring the standard sign mode handler map.
type SignModeOptions struct {
// CoinMetadataQueryFn is the CoinMetadataQueryFn required for SIGN_MODE_TEXTUAL.
CoinMetadataQueryFn textual.CoinMetadataQueryFn
// Textual are options for SIGN_MODE_TEXTUAL
Textual textual.SignModeOptions
}
// HandlerMap returns a sign mode handler map that Cosmos SDK apps can use out
// of the box to support all "standard" sign modes.
func (s SignModeOptions) HandlerMap() (*signing.HandlerMap, error) {
if s.CoinMetadataQueryFn == nil {
return nil, fmt.Errorf("missing %T needed for SIGN_MODE_TEXTUAL", s.CoinMetadataQueryFn)
txt, err := textual.NewSignModeHandler(s.Textual)
if err != nil {
return nil, err
}
return signing.NewHandlerMap(
direct.SignModeHandler{},
textual.NewSignModeHandler(s.CoinMetadataQueryFn),
txt,
), nil
}

View File

@ -3,10 +3,12 @@ package textual
import (
"context"
"fmt"
"strings"
"github.com/cosmos/cosmos-proto/anyutil"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/dynamicpb"
"google.golang.org/protobuf/types/known/anypb"
)
@ -26,17 +28,17 @@ func NewAnyValueRenderer(t *SignModeHandler) ValueRenderer {
// Format implements the ValueRenderer interface.
func (ar anyValueRenderer) Format(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
msg := v.Message().Interface()
omitHeader := 0
anymsg, ok := msg.(*anypb.Any)
if !ok {
return nil, fmt.Errorf("expected Any, got %T", msg)
}
internalMsg, err := anymsg.UnmarshalNew()
anymsg := &anypb.Any{}
err := coerceToMessage(msg, anymsg)
if err != nil {
return nil, fmt.Errorf("error unmarshalling any %s: %w", anymsg.TypeUrl, err)
return nil, err
}
internalMsg, err := anyutil.Unpack(anymsg, ar.tr.fileResolver, ar.tr.typeResolver)
if err != nil {
return nil, err
}
vr, err := ar.tr.GetMessageValueRenderer(internalMsg.ProtoReflect().Descriptor())
if err != nil {
return nil, err
@ -48,6 +50,7 @@ func (ar anyValueRenderer) Format(ctx context.Context, v protoreflect.Value) ([]
}
// The Any value renderer suppresses emission of the object header
omitHeader := 0
_, isMsgRenderer := vr.(*messageValueRenderer)
if isMsgRenderer && subscreens[0].Content == fmt.Sprintf("%s object", internalMsg.ProtoReflect().Descriptor().Name()) {
omitHeader = 1
@ -72,8 +75,22 @@ func (ar anyValueRenderer) Parse(ctx context.Context, screens []Screen) (protore
return nilValue, fmt.Errorf("bad indentation: want 0, got %d", screens[0].Indent)
}
msgType, err := protoregistry.GlobalTypes.FindMessageByURL(screens[0].Content)
if err != nil {
typeURL := screens[0].Content
msgType, err := ar.tr.typeResolver.FindMessageByURL(typeURL)
if err == protoregistry.NotFound {
// If the proto v2 registry doesn't have this message, then we use
// protoFiles (which can e.g. be initialized to gogo's MergedRegistry)
// to retrieve the message descriptor, and then use dynamicpb on that
// message descriptor to create a proto.Message
typeURL := strings.TrimPrefix(typeURL, "/")
msgDesc, err := ar.tr.fileResolver.FindDescriptorByName(protoreflect.FullName(typeURL))
if err != nil {
return nilValue, fmt.Errorf("textual protoFiles does not have descriptor %s: %w", typeURL, err)
}
msgType = dynamicpb.NewMessageType(msgDesc.(protoreflect.MessageDescriptor))
} else if err != nil {
return nilValue, err
}
vr, err := ar.tr.GetMessageValueRenderer(msgType.Descriptor())

View File

@ -6,16 +6,24 @@ import (
"fmt"
"os"
"testing"
"time"
"github.com/cosmos/cosmos-proto/anyutil"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"cosmossdk.io/x/tx/signing/textual"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
"cosmossdk.io/x/tx/internal/testpb"
"cosmossdk.io/x/tx/signing/textual"
)
type anyJsonTest struct {
@ -31,7 +39,8 @@ func TestAny(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
tr := textual.NewSignModeHandler(EmptyCoinMetadataQuerier)
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
for i, tc := range testcases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
anyMsg := anypb.Any{}
@ -55,3 +64,47 @@ func TestAny(t *testing.T) {
})
}
}
func TestDynamicpb(t *testing.T) {
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{
CoinMetadataQuerier: EmptyCoinMetadataQuerier,
TypeResolver: &protoregistry.Types{}, // Set to empty to force using dynamicpb
})
require.NoError(t, err)
testAny, err := anyutil.New(&testpb.Foo{FullName: "foobar"})
require.NoError(t, err)
testcases := []struct {
name string
msg proto.Message
}{
{"coin", &basev1beta1.Coin{Denom: "stake", Amount: "1"}},
{"nested coins", &bankv1beta1.MsgSend{Amount: []*basev1beta1.Coin{{Denom: "stake", Amount: "1"}}}},
{"any", testAny},
{"nested any", &testpb.A{ANY: testAny}},
{"duration", durationpb.New(time.Hour)},
{"timestamp", timestamppb.New(time.Now())},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
any, err := anyutil.New(tc.msg)
require.NoError(t, err)
val := &testpb.A{
ANY: any,
}
vr, err := tr.GetMessageValueRenderer(val.ProtoReflect().Descriptor())
require.NoError(t, err)
// Round trip.
screens, err := vr.Format(context.Background(), protoreflect.ValueOf(val.ProtoReflect()))
require.NoError(t, err)
parsedVal, err := vr.Parse(context.Background(), screens)
require.NoError(t, err)
diff := cmp.Diff(val, parsedVal.Message().Interface(), protocmp.Transform())
require.Empty(t, diff)
})
}
}

View File

@ -21,7 +21,8 @@ func TestBytesJsonTestCases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := textual.NewSignModeHandler(nil)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
for _, tc := range testcases {
t.Run(tc.hex, func(t *testing.T) {

View File

@ -52,18 +52,18 @@ func addMetadataToContext(ctx context.Context, metadata *bankv1beta1.Metadata) c
func TestMetadataQuerier(t *testing.T) {
// Errors on nil metadata querier
txt := textual.NewSignModeHandler(nil)
vr, err := txt.GetFieldValueRenderer(fieldDescriptorFromName("COIN"))
require.NoError(t, err)
_, err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()))
_, err := textual.NewSignModeHandler(textual.SignModeOptions{})
require.Error(t, err)
// Errors if metadata querier returns an error
expErr := fmt.Errorf("mock error")
txt = textual.NewSignModeHandler(func(_ context.Context, _ string) (*bankv1beta1.Metadata, error) {
return nil, expErr
txt, err := textual.NewSignModeHandler(textual.SignModeOptions{
CoinMetadataQuerier: func(_ context.Context, _ string) (*bankv1beta1.Metadata, error) {
return nil, expErr
},
})
vr, err = txt.GetFieldValueRenderer(fieldDescriptorFromName("COIN"))
require.NoError(t, err)
vr, err := txt.GetFieldValueRenderer(fieldDescriptorFromName("COIN"))
require.NoError(t, err)
_, err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()))
require.ErrorIs(t, err, expErr)
@ -78,7 +78,8 @@ func TestCoinJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := textual.NewSignModeHandler(mockCoinMetadataQuerier)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: mockCoinMetadataQuerier})
require.NoError(t, err)
vr, err := textual.GetFieldValueRenderer(fieldDescriptorFromName("COIN"))
require.NoError(t, err)

View File

@ -37,7 +37,11 @@ func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value) (
// Since this value renderer has a FormatRepeated method, the Format one
// here only handles single coin.
coin := v.Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin)
coin := &basev1beta1.Coin{}
err := coerceToMessage(v.Interface().(protoreflect.Message).Interface(), coin)
if err != nil {
return nil, err
}
metadata, err := vr.coinMetadataQuerier(ctx, coin.Denom)
if err != nil {
@ -61,7 +65,11 @@ func (vr coinsValueRenderer) FormatRepeated(ctx context.Context, v protoreflect.
coins, metadatas := make([]*basev1beta1.Coin, protoCoins.Len()), make([]*bankv1beta1.Metadata, protoCoins.Len())
var err error
for i := 0; i < protoCoins.Len(); i++ {
coin := protoCoins.Get(i).Interface().(protoreflect.Message).Interface().(*basev1beta1.Coin)
coin := &basev1beta1.Coin{}
err := coerceToMessage(protoCoins.Get(i).Interface().(protoreflect.Message).Interface(), coin)
if err != nil {
return nil, err
}
coins[i] = coin
metadatas[i], err = vr.coinMetadataQuerier(ctx, coin.Denom)
if err != nil {

View File

@ -22,7 +22,8 @@ func TestCoinsJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
txt := textual.NewSignModeHandler(mockCoinMetadataQuerier)
txt, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: mockCoinMetadataQuerier})
require.NoError(t, err)
vr, err := txt.GetFieldValueRenderer(fieldDescriptorFromName("COINS"))
vrr := vr.(textual.RepeatedValueRenderer)
require.NoError(t, err)

View File

@ -23,7 +23,8 @@ func TestDecJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := textual.NewSignModeHandler(nil)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
for _, tc := range testcases {
tc := tc

View File

@ -67,9 +67,10 @@ func formatSeconds(seconds int64, nanos int32) string {
func (dr durationValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
// Reify the reflected message as a proto Duration
msg := v.Message().Interface()
duration, ok := msg.(*dpb.Duration)
if !ok {
return nil, fmt.Errorf("expected Duration, got %T", msg)
duration := &dpb.Duration{}
err := coerceToMessage(msg, duration)
if err != nil {
return nil, err
}
// Bypass use of time.Duration, as the range is more limited than that of dpb.Duration.

View File

@ -39,7 +39,8 @@ func TestE2EJsonTestcases(t *testing.T) {
t.Run(tc.Name, func(t *testing.T) {
_, bodyBz, _, authInfoBz, signerData := createTextualData(t, tc.Proto, tc.SignerData)
tr := textual.NewSignModeHandler(mockCoinMetadataQuerier)
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: mockCoinMetadataQuerier})
require.NoError(t, err)
rend := textual.NewTxValueRenderer(tr)
ctx := addMetadataToContext(context.Background(), tc.Metadata)

View File

@ -29,7 +29,8 @@ func TestEnumJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := textual.NewSignModeHandler(nil)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
for _, tc := range testcases {
t.Run(tc.Text, func(t *testing.T) {

View File

@ -4,10 +4,12 @@ import (
"bytes"
"context"
"fmt"
"reflect"
signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
@ -27,13 +29,28 @@ type CoinMetadataQueryFn func(ctx context.Context, denom string) (*bankv1beta1.M
// ValueRendererCreator is a function returning a textual.
type ValueRendererCreator func(protoreflect.FieldDescriptor) ValueRenderer
// SignModeHandler holds the configuration for dispatching
// to specific value renderers for SIGN_MODE_TEXTUAL.
type SignModeHandler struct {
// SignModeOptions are options to be passed to Textual's sign mode handler.
type SignModeOptions struct {
// coinMetadataQuerier defines a function to query the coin metadata from
// state. It should use bank module's `DenomsMetadata` gRPC query to fetch
// each denom's associated metadata, either using the bank keeper (for
// server-side code) or a gRPC query client (for client-side code).
CoinMetadataQuerier CoinMetadataQueryFn
// FileResolver are the protobuf files to use for resolving message
// descriptors. If it is nil, the global protobuf registry will be used.
FileResolver *protoregistry.Files
// TypeResolver are the protobuf type resolvers to use for resolving message
// types. If it is nil, then a dynamicpb will be used on top of FileResolver.
TypeResolver protoregistry.MessageTypeResolver
}
// SignModeHandler holds the configuration for dispatching
// to specific value renderers for SIGN_MODE_TEXTUAL.
type SignModeHandler struct {
fileResolver *protoregistry.Files
typeResolver protoregistry.MessageTypeResolver
coinMetadataQuerier CoinMetadataQueryFn
// scalars defines a registry for Cosmos scalars.
scalars map[string]ValueRendererCreator
@ -47,10 +64,25 @@ type SignModeHandler struct {
}
// NewSignModeHandler returns a new SignModeHandler which generates sign bytes and provides value renderers.
func NewSignModeHandler(q CoinMetadataQueryFn) *SignModeHandler {
t := &SignModeHandler{coinMetadataQuerier: q}
func NewSignModeHandler(o SignModeOptions) (*SignModeHandler, error) {
if o.CoinMetadataQuerier == nil {
return nil, fmt.Errorf("coinMetadataQuerier must be non-empty")
}
if o.FileResolver == nil {
o.FileResolver = protoregistry.GlobalFiles
}
if o.TypeResolver == nil {
o.TypeResolver = protoregistry.GlobalTypes
}
t := &SignModeHandler{
coinMetadataQuerier: o.CoinMetadataQuerier,
fileResolver: o.FileResolver,
typeResolver: o.TypeResolver,
}
t.init()
return t
return t, nil
}
// GetFieldValueRenderer returns the value renderer for the given FieldDescriptor.
@ -186,3 +218,48 @@ func (r *SignModeHandler) Mode() signingv1beta1.SignMode {
}
var _ signing.SignModeHandler = &SignModeHandler{}
// getValueFromFieldName is an utility function to get the protoreflect.Value of a
// proto Message from its field name.
func getValueFromFieldName(m proto.Message, fieldName string) protoreflect.Value {
fd := m.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(fieldName))
return m.ProtoReflect().Get(fd)
}
// coerceToMessage initializes the given desiredMsg (presented as a protov2
// concrete message) with the values of givenMsg.
//
// If givenMsg is a protov2 concrete message of the same type, then it will
// fast-path to be initialized to the same pointer value.
// For a dynamicpb message it checks that the names match then uses proto
// reflection to initialize the fields of desiredMsg.
// Otherwise throws an error.
//
// Example:
//
// // Assume `givenCoin` is a dynamicpb.Message representing a Coin
// coin := &basev1beta1.Coin{}
// err := coerceToMessage(givenCoin, coin)
// if err != nil { /* handler error */ }
// fmt.Println(coin) // Will have the same field values as `givenCoin`
func coerceToMessage(givenMsg, desiredMsg proto.Message) error {
if reflect.TypeOf(givenMsg) == reflect.TypeOf(desiredMsg) {
// Below is a way of saying "*desiredMsg = *givenMsg" using go reflect
reflect.Indirect(reflect.ValueOf(desiredMsg)).Set(reflect.Indirect(reflect.ValueOf(givenMsg)))
return nil
}
givenName, desiredName := givenMsg.ProtoReflect().Descriptor().FullName(), desiredMsg.ProtoReflect().Descriptor().FullName()
if givenName != desiredName {
return fmt.Errorf("expected dynamicpb.Message with FullName %s, got %s", desiredName, givenName)
}
desiredFields := desiredMsg.ProtoReflect().Descriptor().Fields()
for i := 0; i < desiredFields.Len(); i++ {
fd := desiredFields.Get(i)
desiredMsg.ProtoReflect().Set(fd, getValueFromFieldName(givenMsg, string(fd.Name())))
}
return nil
}

View File

@ -34,7 +34,8 @@ func TestDispatcher(t *testing.T) {
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
textual := textual.NewSignModeHandler(nil)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
rend, err := textual.GetFieldValueRenderer(fieldDescriptorFromName(tc.name))
if tc.expErr {

View File

@ -23,7 +23,8 @@ func TestIntJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := textual.NewSignModeHandler(nil)
textual, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
require.NoError(t, err)
for _, tc := range testcases {
t.Run(tc[0], func(t *testing.T) {

View File

@ -36,7 +36,7 @@ func TestMessageJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
tr := textual.NewSignModeHandler(EmptyCoinMetadataQuerier)
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: EmptyCoinMetadataQuerier})
for i, tc := range testcases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
rend := textual.NewMessageValueRenderer(tr, (&testpb.Foo{}).ProtoReflect().Descriptor())

View File

@ -29,7 +29,7 @@ func TestRepeatedJsonTestcases(t *testing.T) {
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
tr := textual.NewSignModeHandler(mockCoinMetadataQuerier)
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: mockCoinMetadataQuerier})
for i, tc := range testcases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
// Create a context.Context containing all coins metadata, to simulate

View File

@ -20,15 +20,12 @@ func NewTimestampValueRenderer() ValueRenderer {
// Format implements the ValueRenderer interface.
func (vr timestampValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
// Reify the reflected message as a proto Timestamp
msg := v.Message().Interface()
timestamp, ok := msg.(*tspb.Timestamp)
if !ok {
return nil, fmt.Errorf("expected Timestamp, got %T", msg)
ts := &tspb.Timestamp{}
err := coerceToMessage(v.Message().Interface(), ts)
if err != nil {
return nil, err
}
// Convert proto timestamp to a Go Time.
t := timestamp.AsTime()
t := ts.AsTime()
// Format the Go Time as RFC 3339.
s := t.Format(time.RFC3339Nano)

View File

@ -56,7 +56,7 @@ func TestTxJsonTestcases(t *testing.T) {
t.Run(tc.Name, func(t *testing.T) {
txBody, bodyBz, txAuthInfo, authInfoBz, signerData := createTextualData(t, tc.Proto, tc.SignerData)
tr := textual.NewSignModeHandler(mockCoinMetadataQuerier)
tr, err := textual.NewSignModeHandler(textual.SignModeOptions{CoinMetadataQuerier: mockCoinMetadataQuerier})
rend := textual.NewTxValueRenderer(tr)
ctx := addMetadataToContext(context.Background(), tc.Metadata)