From ef4ad67c9ea209e1f17633918301e04c137a2849 Mon Sep 17 00:00:00 2001 From: Jim Larson <32469398+JimLarson@users.noreply.github.com> Date: Tue, 23 Aug 2022 12:58:29 -0700 Subject: [PATCH] feat: value renderer for timestamp protos (#12860) ## Description Closes: #12709 Part of Sign Mode Textual (ADR 050) implementation. Renders Timestamp messages as RFC 3339 (simplified ISO 8601). --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] added `!` to the type prefix if API or client breaking change - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [x] provided a link to the relevant issue or specification - [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [x] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [x] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- .../adr-050-sign-mode-textual-annex1.md | 15 +- tx/textual/internal/testdata/timestamp.json | 82 ++++++ tx/textual/internal/testpb/1.proto | 2 + tx/textual/internal/testpb/1.pulsar.go | 259 ++++++++++++------ tx/textual/valuerenderer/timestamp.go | 58 ++++ tx/textual/valuerenderer/timestamp_test.go | 93 +++++++ tx/textual/valuerenderer/valuerenderer.go | 32 ++- .../valuerenderer/valuerenderer_test.go | 11 + 8 files changed, 462 insertions(+), 90 deletions(-) create mode 100644 tx/textual/internal/testdata/timestamp.json create mode 100644 tx/textual/valuerenderer/timestamp.go create mode 100644 tx/textual/valuerenderer/timestamp_test.go diff --git a/docs/architecture/adr-050-sign-mode-textual-annex1.md b/docs/architecture/adr-050-sign-mode-textual-annex1.md index 8db3e9fb9f..117d75f2da 100644 --- a/docs/architecture/adr-050-sign-mode-textual-annex1.md +++ b/docs/architecture/adr-050-sign-mode-textual-annex1.md @@ -203,7 +203,20 @@ Object: /cosmos.gov.v1.Vote ### `google.protobuf.Timestamp` -Rendered as either ISO8601 (`2021-01-01T12:00:00Z`). +Rendered using [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) (a +simplification of ISO 8601), which is the current recommendation for portable +time values. The rendering always uses "Z" (UTC) as the timezone. It uses only +the necessary fractional digits of a second, omitting the fractional part +entirely if the timestamp has no fractional seconds. (The resulting timestamps +are not automatically sortable by standard lexicographic order, but we favor +the legibility of the shorter string.) + +#### Examples + +The timestamp with 1136214245 seconds and 700000000 nanoseconds is rendered +as `2006-01-02T15:04:05.7Z`. +The timestamp with 1136214245 seconds and zero nanoseconds is rendered +as `2006-01-02T15:04:05Z`. ### `google.protobuf.Duration` (TODO) diff --git a/tx/textual/internal/testdata/timestamp.json b/tx/textual/internal/testdata/timestamp.json new file mode 100644 index 0000000000..5f868393bf --- /dev/null +++ b/tx/textual/internal/testdata/timestamp.json @@ -0,0 +1,82 @@ +[ + { + "proto": {"seconds": 1136214245}, + "text": "2006-01-02T15:04:05Z" + }, + { + "proto": {"seconds": 1136214245, "nanos": 123456789}, + "text": "2006-01-02T15:04:05.123456789Z" + }, + { + "proto": {"seconds": 1136214245, "nanos": 123000000}, + "text": "2006-01-02T15:04:05.123Z" + }, + { "text": "", "error": true }, + { "text": " ", "error": true }, + { "text": "garbage", "error": true }, + { "text": "11/30/2007", "error": true }, + { + "proto": {"seconds": 0, "nanos": 0}, + "text": "1970-01-01T00:00:00Z" + }, + { + "proto": {"seconds": 0, "nanos": 1}, + "text": "1970-01-01T00:00:00.000000001Z" + }, + { + "proto": {"seconds": 0, "nanos": 10}, + "text": "1970-01-01T00:00:00.00000001Z" + }, + { + "proto": {"seconds": 0, "nanos": 100}, + "text": "1970-01-01T00:00:00.0000001Z" + }, + { + "proto": {"seconds": 0, "nanos": 1000}, + "text": "1970-01-01T00:00:00.000001Z" + }, + { + "proto": {"seconds": 0, "nanos": 10000}, + "text": "1970-01-01T00:00:00.00001Z" + }, + { + "proto": {"seconds": 0, "nanos": 100000}, + "text": "1970-01-01T00:00:00.0001Z" + }, + { + "proto": {"seconds": 0, "nanos": 1000000}, + "text": "1970-01-01T00:00:00.001Z" + }, + { + "proto": {"seconds": 0, "nanos": 10000000}, + "text": "1970-01-01T00:00:00.01Z" + }, + { + "proto": {"seconds": 0, "nanos": 100000000}, + "text": "1970-01-01T00:00:00.1Z" + }, + { + "proto": {"seconds": 0, "nanos": 999999999}, + "text": "1970-01-01T00:00:00.999999999Z" + }, + { + "proto": {"seconds": 1, "nanos": 0}, + "text": "1970-01-01T00:00:01Z" + }, + { + "proto": {"seconds": 2, "nanos": 0}, + "text": "1970-01-01T00:00:02Z" + }, + { + "proto": {"seconds": 2, "nanos": 6}, + "text": "1970-01-01T00:00:02.000000006Z" + }, + { + "proto": {"seconds": 1657797740, "nanos": 983000000}, + "text": "2022-07-14T11:22:20.983Z" + }, + { + "proto": {"seconds": -1, "nanos": 0}, + "text": "1969-12-31T23:59:59Z" + } +] diff --git a/tx/textual/internal/testpb/1.proto b/tx/textual/internal/testpb/1.proto index 8501cc06c6..930b730c7c 100644 --- a/tx/textual/internal/testpb/1.proto +++ b/tx/textual/internal/testpb/1.proto @@ -3,6 +3,7 @@ syntax = "proto3"; option go_package = "cosmossdk.io/tx/textual/internal/testpb"; import "google/protobuf/descriptor.proto"; +import "google/protobuf/timestamp.proto"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; @@ -22,6 +23,7 @@ message A { cosmos.base.v1beta1.Coin COIN = 7; repeated cosmos.base.v1beta1.Coin COINS = 8; bytes BYTES = 9; + google.protobuf.Timestamp TIMESTAMP = 10; } // B contains fields that are not parseable by SIGN_MODE_TEXTUAL, some fields diff --git a/tx/textual/internal/testpb/1.pulsar.go b/tx/textual/internal/testpb/1.pulsar.go index 50aab3696f..2bfa686ad1 100644 --- a/tx/textual/internal/testpb/1.pulsar.go +++ b/tx/textual/internal/testpb/1.pulsar.go @@ -11,6 +11,7 @@ import ( protoiface "google.golang.org/protobuf/runtime/protoiface" protoimpl "google.golang.org/protobuf/runtime/protoimpl" _ "google.golang.org/protobuf/types/descriptorpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" reflect "reflect" @@ -70,16 +71,17 @@ func (x *_A_8_list) IsValid() bool { } var ( - md_A protoreflect.MessageDescriptor - fd_A_UINT32 protoreflect.FieldDescriptor - fd_A_UINT64 protoreflect.FieldDescriptor - fd_A_INT32 protoreflect.FieldDescriptor - fd_A_INT64 protoreflect.FieldDescriptor - fd_A_SDKINT protoreflect.FieldDescriptor - fd_A_SDKDEC protoreflect.FieldDescriptor - fd_A_COIN protoreflect.FieldDescriptor - fd_A_COINS protoreflect.FieldDescriptor - fd_A_BYTES protoreflect.FieldDescriptor + md_A protoreflect.MessageDescriptor + fd_A_UINT32 protoreflect.FieldDescriptor + fd_A_UINT64 protoreflect.FieldDescriptor + fd_A_INT32 protoreflect.FieldDescriptor + fd_A_INT64 protoreflect.FieldDescriptor + fd_A_SDKINT protoreflect.FieldDescriptor + fd_A_SDKDEC protoreflect.FieldDescriptor + fd_A_COIN protoreflect.FieldDescriptor + fd_A_COINS protoreflect.FieldDescriptor + fd_A_BYTES protoreflect.FieldDescriptor + fd_A_TIMESTAMP protoreflect.FieldDescriptor ) func init() { @@ -94,6 +96,7 @@ func init() { fd_A_COIN = md_A.Fields().ByName("COIN") fd_A_COINS = md_A.Fields().ByName("COINS") fd_A_BYTES = md_A.Fields().ByName("BYTES") + fd_A_TIMESTAMP = md_A.Fields().ByName("TIMESTAMP") } var _ protoreflect.Message = (*fastReflection_A)(nil) @@ -215,6 +218,12 @@ func (x *fastReflection_A) Range(f func(protoreflect.FieldDescriptor, protorefle return } } + if x.TIMESTAMP != nil { + value := protoreflect.ValueOfMessage(x.TIMESTAMP.ProtoReflect()) + if !f(fd_A_TIMESTAMP, value) { + return + } + } } // Has reports whether a field is populated. @@ -248,6 +257,8 @@ func (x *fastReflection_A) Has(fd protoreflect.FieldDescriptor) bool { return len(x.COINS) != 0 case "A.BYTES": return len(x.BYTES) != 0 + case "A.TIMESTAMP": + return x.TIMESTAMP != nil default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: A")) @@ -282,6 +293,8 @@ func (x *fastReflection_A) Clear(fd protoreflect.FieldDescriptor) { x.COINS = nil case "A.BYTES": x.BYTES = nil + case "A.TIMESTAMP": + x.TIMESTAMP = nil default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: A")) @@ -328,6 +341,9 @@ func (x *fastReflection_A) Get(descriptor protoreflect.FieldDescriptor) protoref case "A.BYTES": value := x.BYTES return protoreflect.ValueOfBytes(value) + case "A.TIMESTAMP": + value := x.TIMESTAMP + return protoreflect.ValueOfMessage(value.ProtoReflect()) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: A")) @@ -368,6 +384,8 @@ func (x *fastReflection_A) Set(fd protoreflect.FieldDescriptor, value protorefle x.COINS = *clv.list case "A.BYTES": x.BYTES = value.Bytes() + case "A.TIMESTAMP": + x.TIMESTAMP = value.Message().Interface().(*timestamppb.Timestamp) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: A")) @@ -399,6 +417,11 @@ func (x *fastReflection_A) Mutable(fd protoreflect.FieldDescriptor) protoreflect } value := &_A_8_list{list: &x.COINS} return protoreflect.ValueOfList(value) + case "A.TIMESTAMP": + if x.TIMESTAMP == nil { + x.TIMESTAMP = new(timestamppb.Timestamp) + } + return protoreflect.ValueOfMessage(x.TIMESTAMP.ProtoReflect()) case "A.UINT32": panic(fmt.Errorf("field UINT32 of message A is not mutable")) case "A.UINT64": @@ -446,6 +469,9 @@ func (x *fastReflection_A) NewField(fd protoreflect.FieldDescriptor) protoreflec return protoreflect.ValueOfList(&_A_8_list{list: &list}) case "A.BYTES": return protoreflect.ValueOfBytes(nil) + case "A.TIMESTAMP": + m := new(timestamppb.Timestamp) + return protoreflect.ValueOfMessage(m.ProtoReflect()) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: A")) @@ -549,6 +575,10 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } + if x.TIMESTAMP != nil { + l = options.Size(x.TIMESTAMP) + n += 1 + l + runtime.Sov(uint64(l)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -578,6 +608,20 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if x.TIMESTAMP != nil { + encoded, err := options.Marshal(x.TIMESTAMP) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0x52 + } if len(x.BYTES) > 0 { i -= len(x.BYTES) copy(dAtA[i:], x.BYTES) @@ -942,6 +986,42 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods { x.BYTES = []byte{} } iNdEx = postIndex + case 10: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TIMESTAMP", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.TIMESTAMP == nil { + x.TIMESTAMP = ×tamppb.Timestamp{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.TIMESTAMP); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -2162,15 +2242,16 @@ type A struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - UINT32 uint32 `protobuf:"varint,1,opt,name=UINT32,proto3" json:"UINT32,omitempty"` - UINT64 uint64 `protobuf:"varint,2,opt,name=UINT64,proto3" json:"UINT64,omitempty"` - INT32 int32 `protobuf:"varint,3,opt,name=INT32,proto3" json:"INT32,omitempty"` - INT64 int64 `protobuf:"varint,4,opt,name=INT64,proto3" json:"INT64,omitempty"` - SDKINT string `protobuf:"bytes,5,opt,name=SDKINT,proto3" json:"SDKINT,omitempty"` - SDKDEC string `protobuf:"bytes,6,opt,name=SDKDEC,proto3" json:"SDKDEC,omitempty"` - COIN *v1beta1.Coin `protobuf:"bytes,7,opt,name=COIN,proto3" json:"COIN,omitempty"` - COINS []*v1beta1.Coin `protobuf:"bytes,8,rep,name=COINS,proto3" json:"COINS,omitempty"` - BYTES []byte `protobuf:"bytes,9,opt,name=BYTES,proto3" json:"BYTES,omitempty"` + UINT32 uint32 `protobuf:"varint,1,opt,name=UINT32,proto3" json:"UINT32,omitempty"` + UINT64 uint64 `protobuf:"varint,2,opt,name=UINT64,proto3" json:"UINT64,omitempty"` + INT32 int32 `protobuf:"varint,3,opt,name=INT32,proto3" json:"INT32,omitempty"` + INT64 int64 `protobuf:"varint,4,opt,name=INT64,proto3" json:"INT64,omitempty"` + SDKINT string `protobuf:"bytes,5,opt,name=SDKINT,proto3" json:"SDKINT,omitempty"` + SDKDEC string `protobuf:"bytes,6,opt,name=SDKDEC,proto3" json:"SDKDEC,omitempty"` + COIN *v1beta1.Coin `protobuf:"bytes,7,opt,name=COIN,proto3" json:"COIN,omitempty"` + COINS []*v1beta1.Coin `protobuf:"bytes,8,rep,name=COINS,proto3" json:"COINS,omitempty"` + BYTES []byte `protobuf:"bytes,9,opt,name=BYTES,proto3" json:"BYTES,omitempty"` + TIMESTAMP *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=TIMESTAMP,proto3" json:"TIMESTAMP,omitempty"` } func (x *A) Reset() { @@ -2256,6 +2337,13 @@ func (x *A) GetBYTES() []byte { return nil } +func (x *A) GetTIMESTAMP() *timestamppb.Timestamp { + if x != nil { + return x.TIMESTAMP + } + return nil +} + // B contains fields that are not parseable by SIGN_MODE_TEXTUAL, some fields // may be moved to A at some point. type B struct { @@ -2378,57 +2466,62 @@ var File__1_proto protoreflect.FileDescriptor var file__1_proto_rawDesc = []byte{ 0x0a, 0x07, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x63, 0x6f, 0x73, - 0x6d, 0x6f, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x62, - 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x63, 0x6f, 0x69, 0x6e, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa5, 0x02, 0x0a, 0x01, 0x41, 0x12, 0x16, 0x0a, 0x06, - 0x55, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x49, - 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x14, 0x0a, 0x05, - 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x49, 0x4e, 0x54, - 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x26, 0x0a, 0x06, 0x53, 0x44, 0x4b, 0x49, - 0x4e, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0xd2, 0xb4, 0x2d, 0x0a, 0x63, 0x6f, - 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x52, 0x06, 0x53, 0x44, 0x4b, 0x49, 0x4e, 0x54, - 0x12, 0x26, 0x0a, 0x06, 0x53, 0x44, 0x4b, 0x44, 0x45, 0x43, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0e, 0xd2, 0xb4, 0x2d, 0x0a, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x44, 0x65, 0x63, - 0x52, 0x06, 0x53, 0x44, 0x4b, 0x44, 0x45, 0x43, 0x12, 0x2d, 0x0a, 0x04, 0x43, 0x4f, 0x49, 0x4e, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, - 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, - 0x6e, 0x52, 0x04, 0x43, 0x4f, 0x49, 0x4e, 0x12, 0x2f, 0x0a, 0x05, 0x43, 0x4f, 0x49, 0x4e, 0x53, - 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, - 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, - 0x6e, 0x52, 0x05, 0x43, 0x4f, 0x49, 0x4e, 0x53, 0x12, 0x14, 0x0a, 0x05, 0x42, 0x59, 0x54, 0x45, - 0x53, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x42, 0x59, 0x54, 0x45, 0x53, 0x22, 0xd4, - 0x02, 0x0a, 0x01, 0x42, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, - 0x4e, 0x54, 0x33, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x11, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, - 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, - 0x36, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, 0x12, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x36, 0x34, - 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0f, 0x52, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x18, 0x0a, 0x07, - 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x06, 0x20, 0x01, 0x28, 0x07, 0x52, 0x07, 0x46, - 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x12, 0x1a, 0x0a, 0x08, - 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x18, 0x08, 0x20, 0x01, 0x28, 0x10, 0x52, 0x08, - 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45, - 0x44, 0x36, 0x34, 0x18, 0x09, 0x20, 0x01, 0x28, 0x06, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, - 0x36, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x01, 0x52, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x12, 0x1d, 0x0a, 0x03, 0x4d, 0x41, - 0x50, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x42, 0x2e, 0x4d, 0x41, 0x50, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x4d, 0x41, 0x50, 0x1a, 0x3a, 0x0a, 0x08, 0x4d, 0x41, 0x50, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x02, 0x2e, 0x42, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x1f, 0x0a, 0x0b, 0x45, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x54, 0x77, 0x6f, 0x10, 0x01, 0x42, 0x33, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x27, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, - 0x2f, 0x74, 0x78, 0x2f, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, + 0x62, 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x63, 0x6f, 0x69, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdf, 0x02, 0x0a, 0x01, 0x41, 0x12, 0x16, 0x0a, + 0x06, 0x55, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, + 0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x14, 0x0a, + 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x49, 0x4e, + 0x54, 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x26, 0x0a, 0x06, 0x53, 0x44, 0x4b, + 0x49, 0x4e, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0xd2, 0xb4, 0x2d, 0x0a, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x52, 0x06, 0x53, 0x44, 0x4b, 0x49, 0x4e, + 0x54, 0x12, 0x26, 0x0a, 0x06, 0x53, 0x44, 0x4b, 0x44, 0x45, 0x43, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x0e, 0xd2, 0xb4, 0x2d, 0x0a, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x44, 0x65, + 0x63, 0x52, 0x06, 0x53, 0x44, 0x4b, 0x44, 0x45, 0x43, 0x12, 0x2d, 0x0a, 0x04, 0x43, 0x4f, 0x49, + 0x4e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, + 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, + 0x69, 0x6e, 0x52, 0x04, 0x43, 0x4f, 0x49, 0x4e, 0x12, 0x2f, 0x0a, 0x05, 0x43, 0x4f, 0x49, 0x4e, + 0x53, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, + 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, + 0x69, 0x6e, 0x52, 0x05, 0x43, 0x4f, 0x49, 0x4e, 0x53, 0x12, 0x14, 0x0a, 0x05, 0x42, 0x59, 0x54, + 0x45, 0x53, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x42, 0x59, 0x54, 0x45, 0x53, 0x12, + 0x38, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x22, 0xd4, 0x02, 0x0a, 0x01, 0x42, 0x12, + 0x14, 0x0a, 0x05, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, + 0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x11, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x14, 0x0a, + 0x05, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x49, 0x4e, + 0x54, 0x36, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x36, 0x34, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x12, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x36, 0x34, 0x12, 0x1a, 0x0a, 0x08, 0x53, + 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0f, 0x52, 0x08, 0x53, + 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, + 0x33, 0x32, 0x18, 0x06, 0x20, 0x01, 0x28, 0x07, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, + 0x32, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x18, 0x07, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, + 0x44, 0x36, 0x34, 0x18, 0x08, 0x20, 0x01, 0x28, 0x10, 0x52, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, + 0x44, 0x36, 0x34, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x06, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x12, 0x16, 0x0a, + 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x44, + 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x12, 0x1d, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x42, 0x2e, 0x4d, 0x41, 0x50, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x03, 0x4d, 0x41, 0x50, 0x1a, 0x3a, 0x0a, 0x08, 0x4d, 0x41, 0x50, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x18, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x02, 0x2e, 0x42, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x2a, 0x1f, 0x0a, 0x0b, 0x45, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x07, 0x0a, 0x03, 0x4f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x77, 0x6f, 0x10, + 0x01, 0x42, 0x33, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x27, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x74, 0x78, 0x2f, 0x74, + 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2446,22 +2539,24 @@ func file__1_proto_rawDescGZIP() []byte { var file__1_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file__1_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file__1_proto_goTypes = []interface{}{ - (Enumeration)(0), // 0: Enumeration - (*A)(nil), // 1: A - (*B)(nil), // 2: B - nil, // 3: B.MAPEntry - (*v1beta1.Coin)(nil), // 4: cosmos.base.v1beta1.Coin + (Enumeration)(0), // 0: Enumeration + (*A)(nil), // 1: A + (*B)(nil), // 2: B + nil, // 3: B.MAPEntry + (*v1beta1.Coin)(nil), // 4: cosmos.base.v1beta1.Coin + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp } var file__1_proto_depIdxs = []int32{ 4, // 0: A.COIN:type_name -> cosmos.base.v1beta1.Coin 4, // 1: A.COINS:type_name -> cosmos.base.v1beta1.Coin - 3, // 2: B.MAP:type_name -> B.MAPEntry - 2, // 3: B.MAPEntry.value:type_name -> B - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 5, // 2: A.TIMESTAMP:type_name -> google.protobuf.Timestamp + 3, // 3: B.MAP:type_name -> B.MAPEntry + 2, // 4: B.MAPEntry.value:type_name -> B + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file__1_proto_init() } diff --git a/tx/textual/valuerenderer/timestamp.go b/tx/textual/valuerenderer/timestamp.go new file mode 100644 index 0000000000..a14dd7823b --- /dev/null +++ b/tx/textual/valuerenderer/timestamp.go @@ -0,0 +1,58 @@ +package valuerenderer + +import ( + "context" + "fmt" + "io" + "time" + + "google.golang.org/protobuf/reflect/protoreflect" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +type timestampValueRenderer struct{} + +// NewTimestampValueRenderer returns a ValueRenderer for protocol buffer Timestamp messages. +// It renders timestamps using the RFC 3339 format, always using UTC as the timezone. +// Fractional seconds are only rendered if nonzero. +func NewTimestampValueRenderer() ValueRenderer { + return timestampValueRenderer{} +} + +// Format implements the ValueRenderer interface. +func (vr timestampValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) error { + // Reify the reflected message as a proto Timestamp + msg := v.Message().Interface() + timestamp, ok := msg.(*tspb.Timestamp) + if !ok { + return fmt.Errorf("expected Timestamp, got %T", msg) + } + + // Convert proto timestamp to a Go Time. + t := timestamp.AsTime() + + // Format the Go Time as RFC 3339. + s := t.Format(time.RFC3339Nano) + w.Write([]byte(s)) + return nil +} + +// Parse implements the ValueRenderer interface. +func (vr timestampValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) { + // Parse the RFC 3339 input as a Go Time. + bz, err := io.ReadAll(r) + if err != nil { + return protoreflect.Value{}, err + } + t, err := time.Parse(time.RFC3339Nano, string(bz)) + if err != nil { + return protoreflect.Value{}, err + } + + // Convert Go Time to a proto Timestamp. + timestamp := tspb.New(t) + + // Reflect the proto Timestamp. + msg := timestamp.ProtoReflect() + return protoreflect.ValueOfMessage(msg), nil +} diff --git a/tx/textual/valuerenderer/timestamp_test.go b/tx/textual/valuerenderer/timestamp_test.go new file mode 100644 index 0000000000..b4d955905a --- /dev/null +++ b/tx/textual/valuerenderer/timestamp_test.go @@ -0,0 +1,93 @@ +package valuerenderer_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "cosmossdk.io/tx/textual/valuerenderer" + "github.com/stretchr/testify/require" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + dur "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +// timestampJsonTest is the type of test cases in the testdata file. +// If the test case has a Proto, try to Format() it. If Error is set, expect +// an error, otherwise match Text, then Parse() the text and expect it to +// match (via proto.Equals()) the original Proto. If the test case has no +// Proto, try to Parse() the Text and expect an error if Error is set. +// +// The Timestamp proto seconds field is int64, but restricted in range +// by convention and will fit within a JSON number. +type timestampJsonTest struct { + Proto *tspb.Timestamp + Error bool + Text string +} + +func TestTimestampJsonTestcases(t *testing.T) { + raw, err := os.ReadFile("../internal/testdata/timestamp.json") + require.NoError(t, err) + + var testcases []timestampJsonTest + err = json.Unmarshal(raw, &testcases) + require.NoError(t, err) + + for i, tc := range testcases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + rend := valuerenderer.NewTimestampValueRenderer() + + if tc.Proto != nil { + wr := new(strings.Builder) + err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()), wr) + if tc.Error { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.Text, wr.String()) + } + + rd := strings.NewReader(tc.Text) + val, err := rend.Parse(context.Background(), rd) + if tc.Error { + require.Error(t, err) + return + } + require.NoError(t, err) + msg := val.Message().Interface() + require.IsType(t, &tspb.Timestamp{}, msg) + timestamp := msg.(*tspb.Timestamp) + require.True(t, proto.Equal(timestamp, tc.Proto)) + }) + } +} + +func TestTimestampBadFormat(t *testing.T) { + rend := valuerenderer.NewTimestampValueRenderer() + wr := new(strings.Builder) + err := rend.Format(context.Background(), protoreflect.ValueOf(dur.New(time.Hour).ProtoReflect()), wr) + require.Error(t, err) +} + +type badReader struct{} + +var _ io.Reader = badReader{} + +func (br badReader) Read(p []byte) (int, error) { + return 0, fmt.Errorf("reader error") +} + +func TestTimestampBadParse_reader(t *testing.T) { + rend := valuerenderer.NewTimestampValueRenderer() + _, err := rend.Parse(context.Background(), badReader{}) + require.ErrorContains(t, err, "reader error") +} diff --git a/tx/textual/valuerenderer/valuerenderer.go b/tx/textual/valuerenderer/valuerenderer.go index a22e181f95..da4fe0dbf5 100644 --- a/tx/textual/valuerenderer/valuerenderer.go +++ b/tx/textual/valuerenderer/valuerenderer.go @@ -9,18 +9,25 @@ import ( cosmos_proto "github.com/cosmos/cosmos-proto" ) +// Textual holds the configuration for dispatching +// to specific value renderers for SIGN_MODE_TEXTUAL. type Textual struct { - scalars map[string]ValueRenderer + scalars map[string]ValueRenderer + messages map[protoreflect.FullName]ValueRenderer } +// NewTextual returns a new Textual which provides +// value renderers. func NewTextual() Textual { - return Textual{} + t := Textual{} + t.init() + return t } // GetValueRenderer returns the value renderer for the given FieldDescriptor. func (r Textual) GetValueRenderer(fd protoreflect.FieldDescriptor) (ValueRenderer, error) { switch { - // Scalars, such as sdk.Int and sdk.Dec. + // Scalars, such as sdk.Int and sdk.Dec encoded as strings. case fd.Kind() == protoreflect.StringKind && proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar) != "": { scalar, ok := proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar).(string) @@ -28,10 +35,6 @@ func (r Textual) GetValueRenderer(fd protoreflect.FieldDescriptor) (ValueRendere return nil, fmt.Errorf("got extension option %s of type %T", scalar, scalar) } - if r.scalars == nil { - r.init() - } - vr := r.scalars[scalar] if vr == nil { return nil, fmt.Errorf("got empty value renderer for scalar %s", scalar) @@ -51,6 +54,17 @@ func (r Textual) GetValueRenderer(fd protoreflect.FieldDescriptor) (ValueRendere return intValueRenderer{}, nil } + case fd.Kind() == protoreflect.MessageKind: + md := fd.Message() + fullName := md.FullName() + + vr, found := r.messages[fullName] + if found { + return vr, nil + } + // TODO default message renderer + return nil, fmt.Errorf("no value renderer for message %s", fullName) + default: return nil, fmt.Errorf("value renderers cannot format value of type %s", fd.Kind()) } @@ -62,6 +76,10 @@ func (r *Textual) init() { r.scalars["cosmos.Int"] = intValueRenderer{} r.scalars["cosmos.Dec"] = decValueRenderer{} } + if r.messages == nil { + r.messages = map[protoreflect.FullName]ValueRenderer{} + r.messages["google.protobuf.Timestamp"] = NewTimestampValueRenderer() + } } // DefineScalar adds a value renderer to the given Cosmos scalar. diff --git a/tx/textual/valuerenderer/valuerenderer_test.go b/tx/textual/valuerenderer/valuerenderer_test.go index 23d51d84f8..c8d9eeab45 100644 --- a/tx/textual/valuerenderer/valuerenderer_test.go +++ b/tx/textual/valuerenderer/valuerenderer_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/reflect/protoreflect" + tspb "google.golang.org/protobuf/types/known/timestamppb" "cosmossdk.io/math" "cosmossdk.io/tx/textual/internal/testpb" @@ -116,6 +117,14 @@ func TestGetADR050ValueRenderer(t *testing.T) { } } +func TestTimestampDispatch(t *testing.T) { + a := (&testpb.A{}).ProtoReflect().Descriptor().Fields() + textual := valuerenderer.NewTextual() + rend, err := textual.GetValueRenderer(a.ByName(protoreflect.Name("TIMESTAMP"))) + require.NoError(t, err) + require.IsType(t, valuerenderer.NewTimestampValueRenderer(), rend) +} + // valueRendererOf is like GetADR050ValueRenderer, but taking a Go type // as input instead of a protoreflect.FieldDescriptor. func valueRendererOf(v interface{}) (valuerenderer.ValueRenderer, error) { @@ -138,6 +147,8 @@ func valueRendererOf(v interface{}) (valuerenderer.ValueRenderer, error) { return textual.GetValueRenderer(a.ByName(protoreflect.Name("SDKINT"))) case math.LegacyDec: return textual.GetValueRenderer(a.ByName(protoreflect.Name("SDKDEC"))) + case tspb.Timestamp: + return textual.GetValueRenderer(a.ByName(protoreflect.Name("TIMESTAMP"))) // Invalid types for SIGN_MODE_TEXTUAL case float32: