Graphql schema for DAG-JSON objects #1

Merged
ashwin merged 13 commits from ng-gql-ipld into ng-rm-record-types 2024-01-11 09:46:41 +00:00
15 changed files with 7609 additions and 4478 deletions

View File

@ -1,12 +1,5 @@
# Reference to another record. # Reference to another record.
type Reference { scalar Link
id: String! # ID of linked record.
}
# Reference to another record.
input ReferenceInput {
id: String!
}
# Bonds contain funds that are used to pay rent on record registration and renewal. # Bonds contain funds that are used to pay rent on record registration and renewal.
type Bond { type Bond {
@ -37,44 +30,71 @@ type Account {
balance: [Coin!] # Current balance for each coin type. balance: [Coin!] # Current balance for each coin type.
} }
# Value of a given type. # Value describes a DAG-JSON compatible value.
type Value { union Value =
null: Boolean BooleanValue
| IntValue
| FloatValue
| StringValue
| BytesValue
| LinkValue
| ArrayValue
| MapValue
int: Int type BooleanValue {
float: Float value: Boolean!
string: String
boolean: Boolean
json: String
reference: Reference
values: [Value]
} }
# Value of a given type used as input to queries.
input ValueInput {
null: Boolean
int: Int type IntValue {
float: Float value: Int!
string: String }
boolean: Boolean
reference: ReferenceInput type FloatValue {
value: Float!
}
values: [ValueInput] type StringValue {
value: String!
}
type BytesValue {
value: String!
}
type ArrayValue {
value: [Value]!
}
type LinkValue {
value: Link!
}
type MapValue {
value: [Attribute!]!
} }
# Key/value pair. # Key/value pair.
type KeyValue { type Attribute {
key: String! key: String!
value: Value! value: Value
}
# Value of a given type used as input to queries.
# Note: GQL doesn't allow union input types.
input ValueInput {
int: Int
float: Float
string: String
boolean: Boolean
link: Link
array: [ValueInput]
map: [KeyValueInput!]
} }
# Key/value pair for inputs. # Key/value pair for inputs.
input KeyValueInput { input KeyValueInput {
key: String! key: String!
value: ValueInput! value: ValueInput
} }
# Status information about a node (https://docs.tendermint.com/master/rpc/#/Info/status). # Status information about a node (https://docs.tendermint.com/master/rpc/#/Info/status).
@ -155,7 +175,7 @@ type Record {
createTime: String! # Record create time. createTime: String! # Record create time.
expiryTime: String! # Record expiry time. expiryTime: String! # Record expiry time.
owners: [String!] # Addresses of record owners. owners: [String!] # Addresses of record owners.
attributes: [KeyValue] # Record attributes. attributes: [Attribute!] # Record attributes.
references: [Record] # Record references. references: [Record] # Record references.
} }
@ -195,7 +215,7 @@ type Query {
getBondsByIds(ids: [String!]): [Bond] getBondsByIds(ids: [String!]): [Bond]
# Query bonds. # Query bonds.
queryBonds(attributes: [KeyValueInput]): [Bond] queryBonds(attributes: [KeyValueInput!]): [Bond]
# Query bonds by owner. # Query bonds by owner.
queryBondsByOwner(ownerAddresses: [String!]): [OwnerBonds] queryBondsByOwner(ownerAddresses: [String!]): [OwnerBonds]
@ -210,7 +230,7 @@ type Query {
# Query records. # Query records.
queryRecords( queryRecords(
# Multiple attribute conditions are in a logical AND. # Multiple attribute conditions are in a logical AND.
attributes: [KeyValueInput] attributes: [KeyValueInput!]
# Whether to query all records, not just named ones (false by default). # Whether to query all records, not just named ones (false by default).
all: Boolean all: Boolean

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
# .gqlgen.yml example
#
# Refer to https://gqlgen.com/config/ # Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation. # for detailed .gqlgen.yml documentation.
@ -12,3 +10,8 @@ model:
resolver: resolver:
filename: resolver.go filename: resolver.go
type: Resolver type: Resolver
models:
Link:
model:
- github.com/cerc-io/laconicd/gql.Link

View File

@ -2,6 +2,10 @@
package gql package gql
type Value interface {
IsValue()
}
type Account struct { type Account struct {
Address string `json:"address"` Address string `json:"address"`
PubKey *string `json:"pubKey"` PubKey *string `json:"pubKey"`
@ -10,6 +14,17 @@ type Account struct {
Balance []*Coin `json:"balance"` Balance []*Coin `json:"balance"`
} }
type ArrayValue struct {
Value []Value `json:"value"`
}
func (ArrayValue) IsValue() {}
type Attribute struct {
Key string `json:"key"`
Value Value `json:"value"`
}
type Auction struct { type Auction struct {
ID string `json:"id"` ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
@ -53,21 +68,52 @@ type Bond struct {
Balance []*Coin `json:"balance"` Balance []*Coin `json:"balance"`
} }
type BooleanValue struct {
Value bool `json:"value"`
}
func (BooleanValue) IsValue() {}
type BytesValue struct {
Value string `json:"value"`
}
func (BytesValue) IsValue() {}
type Coin struct { type Coin struct {
Type string `json:"type"` Type string `json:"type"`
Quantity string `json:"quantity"` Quantity string `json:"quantity"`
} }
type KeyValue struct { type FloatValue struct {
Key string `json:"key"` Value float64 `json:"value"`
Value *Value `json:"value"`
} }
func (FloatValue) IsValue() {}
type IntValue struct {
Value int `json:"value"`
}
func (IntValue) IsValue() {}
type KeyValueInput struct { type KeyValueInput struct {
Key string `json:"key"` Key string `json:"key"`
Value *ValueInput `json:"value"` Value *ValueInput `json:"value"`
} }
type LinkValue struct {
Value Link `json:"value"`
}
func (LinkValue) IsValue() {}
type MapValue struct {
Value []*Attribute `json:"value"`
}
func (MapValue) IsValue() {}
type NameRecord struct { type NameRecord struct {
Latest *NameRecordEntry `json:"latest"` Latest *NameRecordEntry `json:"latest"`
History []*NameRecordEntry `json:"history"` History []*NameRecordEntry `json:"history"`
@ -102,18 +148,10 @@ type Record struct {
CreateTime string `json:"createTime"` CreateTime string `json:"createTime"`
ExpiryTime string `json:"expiryTime"` ExpiryTime string `json:"expiryTime"`
Owners []string `json:"owners"` Owners []string `json:"owners"`
Attributes []*KeyValue `json:"attributes"` Attributes []*Attribute `json:"attributes"`
References []*Record `json:"references"` References []*Record `json:"references"`
} }
type Reference struct {
ID string `json:"id"`
}
type ReferenceInput struct {
ID string `json:"id"`
}
type Status struct { type Status struct {
Version string `json:"version"` Version string `json:"version"`
Node *NodeInfo `json:"node"` Node *NodeInfo `json:"node"`
@ -125,6 +163,12 @@ type Status struct {
DiskUsage string `json:"disk_usage"` DiskUsage string `json:"disk_usage"`
} }
type StringValue struct {
Value string `json:"value"`
}
func (StringValue) IsValue() {}
type SyncInfo struct { type SyncInfo struct {
LatestBlockHash string `json:"latest_block_hash"` LatestBlockHash string `json:"latest_block_hash"`
LatestBlockHeight string `json:"latest_block_height"` LatestBlockHeight string `json:"latest_block_height"`
@ -138,23 +182,12 @@ type ValidatorInfo struct {
ProposerPriority *string `json:"proposer_priority"` ProposerPriority *string `json:"proposer_priority"`
} }
type Value struct {
Null *bool `json:"null"`
Int *int `json:"int"`
Float *float64 `json:"float"`
String *string `json:"string"`
Boolean *bool `json:"boolean"`
JSON *string `json:"json"`
Reference *Reference `json:"reference"`
Values []*Value `json:"values"`
}
type ValueInput struct { type ValueInput struct {
Null *bool `json:"null"`
Int *int `json:"int"` Int *int `json:"int"`
Float *float64 `json:"float"` Float *float64 `json:"float"`
String *string `json:"string"` String *string `json:"string"`
Boolean *bool `json:"boolean"` Boolean *bool `json:"boolean"`
Reference *ReferenceInput `json:"reference"` Link *Link `json:"link"`
Values []*ValueInput `json:"values"` Array []*ValueInput `json:"array"`
Map []*KeyValueInput `json:"map"`
} }

View File

@ -121,7 +121,7 @@ func (q queryResolver) QueryRecords(ctx context.Context, attributes []*KeyValueI
res, err := nsQueryClient.ListRecords( res, err := nsQueryClient.ListRecords(
context.Background(), context.Background(),
&registrytypes.QueryListRecordsRequest{ &registrytypes.QueryListRecordsRequest{
Attributes: parseRequestAttributes(attributes), Attributes: toRPCAttributes(attributes),
All: (all != nil && *all), All: (all != nil && *all),
}, },
) )

33
gql/scalar.go Normal file
View File

@ -0,0 +1,33 @@
package gql
import (
"context"
"encoding/json"
"fmt"
"io"
)
// Represents an IPLD link. Links are generally but not necessarily implemented as CIDs
type Link string
func (l Link) String() string {
return string(l)
}
// UnmarshalGQLContext implements the graphql.ContextUnmarshaler interface
func (l *Link) UnmarshalGQLContext(_ context.Context, v interface{}) error {
s, ok := v.(string)
if !ok {
return fmt.Errorf("Link must be a string")
}
*l = Link(s)
return nil
}
// MarshalGQLContext implements the graphql.ContextMarshaler interface
func (l Link) MarshalGQLContext(_ context.Context, w io.Writer) error {
encodable := map[string]string{
"/": l.String(),
}
return json.NewEncoder(w).Encode(encodable)
}

View File

@ -2,15 +2,15 @@ package gql
import ( import (
"context" "context"
"encoding/json" "fmt" // #nosec G702
"fmt"
"reflect" // #nosec G702
"strconv" "strconv"
auctiontypes "github.com/cerc-io/laconicd/x/auction/types" auctiontypes "github.com/cerc-io/laconicd/x/auction/types"
bondtypes "github.com/cerc-io/laconicd/x/bond/types" bondtypes "github.com/cerc-io/laconicd/x/bond/types"
registrytypes "github.com/cerc-io/laconicd/x/registry/types" registrytypes "github.com/cerc-io/laconicd/x/registry/types"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
) )
// OwnerAttributeName denotes the owner attribute name for a bond. // OwnerAttributeName denotes the owner attribute name for a bond.
@ -61,13 +61,21 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
return nil, nil return nil, nil
} }
recordType := record.ToReadableRecord() node, err := ipld.Decode(record.Attributes, dagjson.Decode)
attributes, err := getAttributes(&recordType) if err != nil {
return nil, err
}
if node.Kind() != ipld.Kind_Map {
return nil, fmt.Errorf("invalid record attributes")
}
var links []string
attributes, err := resolveIPLDNode(node, &links)
if err != nil { if err != nil {
return nil, err return nil, err
} }
references, err := getReferences(ctx, resolver, &recordType) references, err := resolver.GetRecordsByIds(ctx, links)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,11 +87,96 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
ExpiryTime: record.GetExpiryTime(), ExpiryTime: record.GetExpiryTime(),
Owners: record.GetOwners(), Owners: record.GetOwners(),
Names: record.GetNames(), Names: record.GetNames(),
Attributes: attributes, Attributes: attributes.(MapValue).Value,
References: references, References: references,
}, nil }, nil
} }
func resolveIPLDNode(node ipld.Node, links *[]string) (Value, error) {
switch node.Kind() {
case ipld.Kind_Map:
var entries []*Attribute
for itr := node.MapIterator(); !itr.Done(); {
k, v, err := itr.Next()
if err != nil {
return nil, err
}
if k.Kind() != ipld.Kind_String {
return nil, fmt.Errorf("invalid record attribute key type: %s", k.Kind())
}
s, err := k.AsString()
if err != nil {
return nil, err
}
val, err := resolveIPLDNode(v, links)
if err != nil {
return nil, err
}
entries = append(entries, &Attribute{
Key: s,
Value: val,
})
}
return MapValue{entries}, nil
case ipld.Kind_List:
var values []Value
for itr := node.ListIterator(); !itr.Done(); {
_, v, err := itr.Next()
if err != nil {
return nil, err
}
val, err := resolveIPLDNode(v, links)
if err != nil {
return nil, err
}
values = append(values, val)
}
return ArrayValue{values}, nil
case ipld.Kind_Null:
return nil, nil
case ipld.Kind_Bool:
val, err := node.AsBool()
if err != nil {
return nil, err
}
return BooleanValue{val}, nil
case ipld.Kind_Int:
val, err := node.AsInt()
if err != nil {
return nil, err
}
// TODO: handle bigger ints
return IntValue{int(val)}, nil
case ipld.Kind_Float:
val, err := node.AsFloat()
if err != nil {
return nil, err
}
return FloatValue{val}, nil
case ipld.Kind_String:
val, err := node.AsString()
if err != nil {
return nil, err
}
return StringValue{val}, nil
case ipld.Kind_Bytes:
val, err := node.AsBytes()
if err != nil {
return nil, err
}
return BytesValue{string(val)}, nil
case ipld.Kind_Link:
val, err := node.AsLink()
if err != nil {
return nil, err
}
*links = append(*links, val.String())
return LinkValue{Link(val.String())}, nil
default:
return nil, fmt.Errorf("invalid node kind")
}
}
func getGQLNameRecord(record *registrytypes.NameRecord) (*NameRecord, error) { func getGQLNameRecord(record *registrytypes.NameRecord) (*NameRecord, error) {
if record == nil { if record == nil {
return nil, fmt.Errorf("got nil record") return nil, fmt.Errorf("got nil record")
@ -163,136 +256,47 @@ func GetGQLAuction(auction *auctiontypes.Auction, bids []*auctiontypes.Bid) (*Au
return &gqlAuction, nil return &gqlAuction, nil
} }
func getReferences(ctx context.Context, resolver QueryResolver, r *registrytypes.ReadableRecord) ([]*Record, error) { func toRPCValue(value *ValueInput) *registrytypes.QueryListRecordsRequest_ValueInput {
var ids []string var rpcval registrytypes.QueryListRecordsRequest_ValueInput
// #nosec G705 switch {
for key := range r.Attributes { case value == nil:
//nolint: all return nil
switch r.Attributes[key].(type) { case value.Int != nil:
case interface{}: rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Int{Int: int64(*value.Int)}
if obj, ok := r.Attributes[key].(map[string]interface{}); ok { case value.Float != nil:
if _, ok := obj["/"]; ok && len(obj) == 1 { rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Float{Float: *value.Float}
if _, ok := obj["/"].(string); ok { case value.String != nil:
ids = append(ids, obj["/"].(string)) rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_String_{String_: *value.String}
case value.Boolean != nil:
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Boolean{Boolean: *value.Boolean}
case value.Link != nil:
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Link{Link: value.Link.String()}
case value.Array != nil:
var contents registrytypes.QueryListRecordsRequest_ArrayInput
for _, val := range value.Array {
contents.Values = append(contents.Values, toRPCValue(val))
} }
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Array{Array: &contents}
case value.Map != nil:
var contents registrytypes.QueryListRecordsRequest_MapInput
for _, kv := range value.Map {
contents.Values[kv.Key] = toRPCValue(kv.Value)
} }
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Map{Map: &contents}
} }
} return &rpcval
}
return resolver.GetRecordsByIds(ctx, ids)
} }
func getAttributes(r *registrytypes.ReadableRecord) ([]*KeyValue, error) { func toRPCAttributes(attrs []*KeyValueInput) []*registrytypes.QueryListRecordsRequest_KeyValueInput {
return mapToKeyValuePairs(r.Attributes)
}
func mapToKeyValuePairs(attrs map[string]interface{}) ([]*KeyValue, error) {
kvPairs := []*KeyValue{}
trueVal := true
falseVal := false
// #nosec G705
for key, value := range attrs {
kvPair := &KeyValue{
Key: key,
Value: &Value{},
}
switch val := value.(type) {
case nil:
kvPair.Value.Null = &trueVal
case int:
kvPair.Value.Int = &val
case float64:
kvPair.Value.Float = &val
case string:
kvPair.Value.String = &val
case bool:
kvPair.Value.Boolean = &val
case interface{}:
if obj, ok := value.(map[string]interface{}); ok {
if _, ok := obj["/"]; ok && len(obj) == 1 {
if _, ok := obj["/"].(string); ok {
kvPair.Value.Reference = &Reference{
ID: obj["/"].(string),
}
}
} else {
bytes, err := json.Marshal(obj)
if err != nil {
return nil, err
}
jsonStr := string(bytes)
kvPair.Value.JSON = &jsonStr
}
}
}
if kvPair.Value.Null == nil {
kvPair.Value.Null = &falseVal
}
valueType := reflect.ValueOf(value)
if valueType.Kind() == reflect.Slice {
bytes, err := json.Marshal(value)
if err != nil {
return nil, err
}
jsonStr := string(bytes)
kvPair.Value.JSON = &jsonStr
}
kvPairs = append(kvPairs, kvPair)
}
return kvPairs, nil
}
func parseRequestAttributes(attrs []*KeyValueInput) []*registrytypes.QueryListRecordsRequest_KeyValueInput {
kvPairs := []*registrytypes.QueryListRecordsRequest_KeyValueInput{} kvPairs := []*registrytypes.QueryListRecordsRequest_KeyValueInput{}
for _, value := range attrs { for _, value := range attrs {
parsedValue := toRPCValue(value.Value)
kvPair := &registrytypes.QueryListRecordsRequest_KeyValueInput{ kvPair := &registrytypes.QueryListRecordsRequest_KeyValueInput{
Key: value.Key, Key: value.Key,
Value: &registrytypes.QueryListRecordsRequest_ValueInput{}, Value: parsedValue,
} }
if value.Value.String != nil {
kvPair.Value.String_ = *value.Value.String
kvPair.Value.Type = "string"
}
if value.Value.Int != nil {
kvPair.Value.Int = int64(*value.Value.Int)
kvPair.Value.Type = "int"
}
if value.Value.Float != nil {
kvPair.Value.Float = *value.Value.Float
kvPair.Value.Type = "float"
}
if value.Value.Boolean != nil {
kvPair.Value.Boolean = *value.Value.Boolean
kvPair.Value.Type = "boolean"
}
if value.Value.Reference != nil {
reference := &registrytypes.QueryListRecordsRequest_ReferenceInput{
Id: value.Value.Reference.ID,
}
kvPair.Value.Reference = reference
kvPair.Value.Type = "reference"
}
// TODO: Handle arrays.
kvPairs = append(kvPairs, kvPair) kvPairs = append(kvPairs, kvPair)
} }

View File

@ -67,17 +67,26 @@ message QueryParamsResponse {
// QueryListRecordsRequest is request type for registry records list // QueryListRecordsRequest is request type for registry records list
message QueryListRecordsRequest { message QueryListRecordsRequest {
message ReferenceInput { message LinkInput {
string id = 1; string id = 1;
} }
message ArrayInput {
repeated ValueInput values = 1;
}
message MapInput {
map<string, ValueInput> values = 1;
}
message ValueInput { message ValueInput {
string type = 1; // Type of record attribute value
string string = 2; oneof value {
int64 int = 3; string string = 1;
double float = 4; int64 int = 2;
bool boolean = 5; double float = 3;
ReferenceInput reference = 6; bool boolean = 4;
repeated ValueInput values = 7; string link = 5;
ArrayInput array = 6;
MapInput map = 7;
}
} }
message KeyValueInput { message KeyValueInput {
string key = 1; string key = 1;

View File

@ -47,24 +47,20 @@ func Int64ToBytes(num int64) []byte {
return buf.Bytes() return buf.Bytes()
} }
// MarshalMapToJSONBytes converts map[string]interface{} to bytes. func MustMarshalJSON[T any](val T) (bytes []byte) {
func MarshalMapToJSONBytes(val map[string]interface{}) (bytes []byte) {
bytes, err := json.Marshal(val) bytes, err := json.Marshal(val)
if err != nil { if err != nil {
panic("Marshal error.") panic("JSON marshal error:" + err.Error())
} }
return return
} }
// UnMarshalMapFromJSONBytes converts bytes to map[string]interface{}. func MustUnmarshalJSON[T any](bytes []byte) T {
func UnMarshalMapFromJSONBytes(bytes []byte) map[string]interface{} { var val T
var val map[string]interface{}
err := json.Unmarshal(bytes, &val) err := json.Unmarshal(bytes, &val)
if err != nil { if err != nil {
panic("Unmarshal error.") panic("JSON unmarshal error:" + err.Error())
} }
return val return val
} }

View File

@ -3,12 +3,13 @@ package keeper_test
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"reflect"
"github.com/cerc-io/laconicd/x/registry/client/cli" "github.com/cerc-io/laconicd/x/registry/client/cli"
"github.com/cerc-io/laconicd/x/registry/helpers" "github.com/cerc-io/laconicd/x/registry/helpers"
"github.com/cerc-io/laconicd/x/registry/keeper" "github.com/cerc-io/laconicd/x/registry/keeper"
registrytypes "github.com/cerc-io/laconicd/x/registry/types" registrytypes "github.com/cerc-io/laconicd/x/registry/types"
"os"
"reflect"
) )
func (suite *KeeperTestSuite) TestGrpcQueryParams() { func (suite *KeeperTestSuite) TestGrpcQueryParams() {
@ -69,26 +70,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{ {
Key: "type", Key: "type",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{ Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string", Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"WebsiteRegistrationRecord"},
String_: "WebsiteRegistrationRecord",
},
},
},
All: true,
},
true,
false,
1,
},
{
"Filter with tag (extant) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
&registrytypes.QueryListRecordsRequest{
Attributes: []*registrytypes.QueryListRecordsRequest_KeyValueInput{
{
Key: "tags",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string",
String_: "tagA",
}, },
}, },
}, },
@ -98,6 +80,35 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
false, false,
1, 1,
}, },
// Skip the following test as querying with recursive values not supported (PR https://git.vdb.to/cerc-io/laconicd/pulls/112)
// See function RecordsFromAttributes (QueryValueToJSON call) in the registry keeper implementation (x/registry/keeper/keeper.go)
// {
// "Filter with tag (extant) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
// &registrytypes.QueryListRecordsRequest{
// Attributes: []*registrytypes.QueryListRecordsRequest_KeyValueInput{
// {
// Key: "tags",
// // Value: &registrytypes.QueryListRecordsRequest_ValueInput{
// // Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"tagA"},
// // },
// Value: &registrytypes.QueryListRecordsRequest_ValueInput{
// Value: &registrytypes.QueryListRecordsRequest_ValueInput_Array{Array: &registrytypes.QueryListRecordsRequest_ArrayInput{
// Values: []*registrytypes.QueryListRecordsRequest_ValueInput{
// {
// Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"tagA"},
// },
// },
// }},
// },
// // Throws: "Recursive query values are not supported"
// },
// },
// All: true,
// },
// true,
// false,
// 1,
// },
{ {
"Filter with tag (non-existent) (https://git.vdb.to/cerc-io/laconicd/issues/129)", "Filter with tag (non-existent) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
&registrytypes.QueryListRecordsRequest{ &registrytypes.QueryListRecordsRequest{
@ -105,8 +116,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{ {
Key: "tags", Key: "tags",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{ Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string", Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"NOEXIST"},
String_: "NOEXIST",
}, },
}, },
}, },
@ -123,8 +133,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{ {
Key: "typ", Key: "typ",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{ Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string", Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"eWebsiteRegistrationRecord"},
String_: "eWebsiteRegistrationRecord",
}, },
}, },
}, },
@ -141,8 +150,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{ {
Key: "x500state_name", Key: "x500state_name",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{ Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string", Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"california"},
String_: "california",
}, },
}, },
}, },
@ -183,9 +191,13 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
sr.Equal(resp.GetRecords()[0].GetBondId(), suite.bond.GetId()) sr.Equal(resp.GetRecords()[0].GetBondId(), suite.bond.GetId())
for _, record := range resp.GetRecords() { for _, record := range resp.GetRecords() {
recAttr := helpers.UnMarshalMapFromJSONBytes(record.Attributes) recAttr := helpers.MustUnmarshalJSON[registrytypes.AttributeMap](record.Attributes)
for _, attr := range test.req.GetAttributes() { for _, attr := range test.req.GetAttributes() {
av := keeper.GetAttributeValue(attr.Value) enc, err := keeper.QueryValueToJSON(attr.Value)
sr.NoError(err)
av := helpers.MustUnmarshalJSON[any](enc)
if nil != av && nil != recAttr[attr.Key] && if nil != av && nil != recAttr[attr.Key] &&
reflect.Slice == reflect.TypeOf(recAttr[attr.Key]).Kind() && reflect.Slice == reflect.TypeOf(recAttr[attr.Key]).Kind() &&
reflect.Slice != reflect.TypeOf(av).Kind() { reflect.Slice != reflect.TypeOf(av).Kind() {

View File

@ -8,10 +8,6 @@ import (
"time" "time"
errorsmod "cosmossdk.io/errors" errorsmod "cosmossdk.io/errors"
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
"github.com/cerc-io/laconicd/x/registry/helpers"
"github.com/cerc-io/laconicd/x/registry/types"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/legacy" "github.com/cosmos/cosmos-sdk/codec/legacy"
storetypes "github.com/cosmos/cosmos-sdk/store/types" storetypes "github.com/cosmos/cosmos-sdk/store/types"
@ -20,7 +16,18 @@ import (
auth "github.com/cosmos/cosmos-sdk/x/auth/keeper" auth "github.com/cosmos/cosmos-sdk/x/auth/keeper"
bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/gibson042/canonicaljson-go"
cid "github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
"github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/log"
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
"github.com/cerc-io/laconicd/x/registry/helpers"
"github.com/cerc-io/laconicd/x/registry/types"
) )
var ( var (
@ -99,6 +106,10 @@ func NewKeeper(cdc codec.BinaryCodec, accountKeeper auth.AccountKeeper, bankKeep
// Logger returns a module-specific logger. // Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger { func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return logger(ctx)
}
func logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", types.ModuleName) return ctx.Logger().With("module", types.ModuleName)
} }
@ -146,8 +157,11 @@ func (k Keeper) ListRecords(ctx sdk.Context) []types.Record {
func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.QueryListRecordsRequest_KeyValueInput, all bool) ([]types.Record, error) { func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.QueryListRecordsRequest_KeyValueInput, all bool) ([]types.Record, error) {
resultRecordIds := []string{} resultRecordIds := []string{}
for i, attr := range attributes { for i, attr := range attributes {
val := GetAttributeValue(attr.Value) suffix, err := QueryValueToJSON(attr.Value)
attributeIndex := GetAttributesIndexKey(attr.Key, val) if err != nil {
return nil, err
}
attributeIndex := GetAttributesIndexKey(attr.Key, suffix)
recordIds, err := k.GetAttributeMapping(ctx, attributeIndex) recordIds, err := k.GetAttributeMapping(ctx, attributeIndex)
if err != nil { if err != nil {
return nil, err return nil, err
@ -175,23 +189,53 @@ func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.Query
return records, nil return records, nil
} }
func GetAttributeValue(input *types.QueryListRecordsRequest_ValueInput) interface{} { // TODO not recursive, and only should be if we want to support querying with whole sub-objects,
if input.Type == "int" { // which seems unnecessary.
return input.GetInt() func QueryValueToJSON(input *types.QueryListRecordsRequest_ValueInput) ([]byte, error) {
np := basicnode.Prototype.Any
nb := np.NewBuilder()
switch value := input.GetValue().(type) {
case *types.QueryListRecordsRequest_ValueInput_String_:
err := nb.AssignString(value.String_)
if err != nil {
return nil, err
} }
if input.Type == "float" { case *types.QueryListRecordsRequest_ValueInput_Int:
return input.GetFloat() err := nb.AssignInt(value.Int)
if err != nil {
return nil, err
} }
if input.Type == "string" { case *types.QueryListRecordsRequest_ValueInput_Float:
return input.GetString_() err := nb.AssignFloat(value.Float)
if err != nil {
return nil, err
} }
if input.Type == "boolean" { case *types.QueryListRecordsRequest_ValueInput_Boolean:
return input.GetBoolean() err := nb.AssignBool(value.Boolean)
if err != nil {
return nil, err
} }
if input.Type == "reference" { case *types.QueryListRecordsRequest_ValueInput_Link:
return input.GetReference().GetId() link := cidlink.Link{Cid: cid.MustParse(value.Link)}
err := nb.AssignLink(link)
if err != nil {
return nil, err
} }
return nil case *types.QueryListRecordsRequest_ValueInput_Array:
return nil, fmt.Errorf("recursive query values are not supported")
case *types.QueryListRecordsRequest_ValueInput_Map:
return nil, fmt.Errorf("recursive query values are not supported")
default:
return nil, fmt.Errorf("value has unexpected type %T", value)
}
n := nb.Build()
var buf bytes.Buffer
if err := dagjson.Encode(n, &buf); err != nil {
return nil, fmt.Errorf("encoding value to JSON failed: %w", err)
}
return buf.Bytes(), nil
} }
func getIntersection(a []string, b []string) []string { func getIntersection(a []string, b []string) []string {
@ -244,7 +288,7 @@ func (k Keeper) GetRecordExpiryQueue(ctx sdk.Context) []*types.ExpiryQueueRecord
// ProcessSetRecord creates a record. // ProcessSetRecord creates a record.
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.ReadableRecord, error) { func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.ReadableRecord, error) {
payload := msg.Payload.ToReadablePayload() payload := msg.Payload.ToReadablePayload()
record := types.ReadableRecord{Attributes: payload.Record, BondID: msg.BondId} record := types.ReadableRecord{Attributes: payload.RecordAttributes, BondID: msg.BondId}
// Check signatures. // Check signatures.
resourceSignBytes, _ := record.GetSignBytes() resourceSignBytes, _ := record.GetSignBytes()
@ -264,14 +308,12 @@ func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*type
for _, sig := range payload.Signatures { for _, sig := range payload.Signatures {
pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey)) pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey))
if err != nil { if err != nil {
fmt.Println("Error decoding pubKey from bytes: ", err) return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err))
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid public key.")
} }
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig)) sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
if !sigOK { if !sigOK {
fmt.Println("Signature mismatch: ", sig.PubKey) return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey))
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid signature.")
} }
record.Owners = append(record.Owners, pubKey.Address().String()) record.Owners = append(record.Owners, pubKey.Address().String())
} }
@ -312,7 +354,7 @@ func (k Keeper) processRecord(ctx sdk.Context, record *types.ReadableRecord, isR
return err return err
} }
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, record.ExpiryTime) expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime))
if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil { if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil {
return err return err
} }
@ -334,14 +376,51 @@ func (k Keeper) PutRecord(ctx sdk.Context, record types.Record) {
k.updateBlockChangeSetForRecord(ctx, record.Id) k.updateBlockChangeSetForRecord(ctx, record.Id)
} }
func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id string, prefix string) error { func (k Keeper) processAttributes(ctx sdk.Context, attrs types.AttributeMap, id string, prefix string) error {
for key, value := range attrs { np := basicnode.Prototype.Map
if subRecord, ok := value.(map[string]any); ok { nb := np.NewBuilder()
err := k.processAttributes(ctx, subRecord, id, key) encAttrs, err := canonicaljson.Marshal(attrs)
if err != nil {
return err
}
if len(attrs) == 0 {
encAttrs = []byte("{}")
}
err = dagjson.Decode(nb, bytes.NewReader(encAttrs))
if err != nil {
return fmt.Errorf("failed to decode attributes: %w", err)
}
n := nb.Build()
if n.Kind() != ipld.Kind_Map {
return fmt.Errorf("record attributes must be a map, not %T", n.Kind())
}
return k.processAttributeMap(ctx, n, id, prefix)
}
func (k Keeper) processAttributeMap(ctx sdk.Context, n ipld.Node, id string, prefix string) error {
for it := n.MapIterator(); !it.Done(); {
//nolint:misspell
keynode, valuenode, err := it.Next()
if err != nil {
return err
}
key, err := keynode.AsString()
if err != nil {
return err
}
if valuenode.Kind() == ipld.Kind_Map {
err := k.processAttributeMap(ctx, valuenode, id, key)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
var buf bytes.Buffer
if err := dagjson.Encode(valuenode, &buf); err != nil {
return err
}
value := buf.Bytes()
indexKey := GetAttributesIndexKey(prefix+key, value) indexKey := GetAttributesIndexKey(prefix+key, value)
if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil { if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil {
return err return err
@ -351,8 +430,8 @@ func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id stri
return nil return nil
} }
func GetAttributesIndexKey(key string, value interface{}) []byte { func GetAttributesIndexKey(key string, suffix []byte) []byte {
keyString := fmt.Sprintf("%s=%s", key, value) keyString := fmt.Sprintf("%s=%s", key, suffix)
return append(PrefixAttributesIndex, []byte(keyString)...) return append(PrefixAttributesIndex, []byte(keyString)...)
} }
@ -364,8 +443,6 @@ func (k Keeper) SetAttributeMapping(ctx sdk.Context, key []byte, recordID string
if err != nil { if err != nil {
return fmt.Errorf("cannot unmarshal byte array, error, %w", err) return fmt.Errorf("cannot unmarshal byte array, error, %w", err)
} }
} else {
recordIds = []string{}
} }
recordIds = append(recordIds, recordID) recordIds = append(recordIds, recordID)
bz, err := json.Marshal(recordIds) bz, err := json.Marshal(recordIds)

View File

@ -611,7 +611,7 @@ func (k Keeper) ProcessAuthorityExpiryQueue(ctx sdk.Context) {
k.SetNameAuthority(ctx, name, &authority) k.SetNameAuthority(ctx, name, &authority)
k.DeleteAuthorityExpiryQueue(ctx, name, authority) k.DeleteAuthorityExpiryQueue(ctx, name, authority)
ctx.Logger().Info(fmt.Sprintf("Marking authority expired as no bond present: %s", name)) k.Logger(ctx).Info(fmt.Sprintf("Marking authority expired as no bond present: %s", name))
return return
} }
@ -672,7 +672,7 @@ func (k Keeper) AuthorityExpiryQueueIterator(ctx sdk.Context, endTime time.Time)
// TryTakeAuthorityRent tries to take rent from the authority bond. // TryTakeAuthorityRent tries to take rent from the authority bond.
func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority types.NameAuthority) { func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority types.NameAuthority) {
ctx.Logger().Info(fmt.Sprintf("Trying to take rent for authority: %s", name)) k.Logger(ctx).Info(fmt.Sprintf("Trying to take rent for authority: %s", name))
params := k.GetParams(ctx) params := k.GetParams(ctx)
rent := params.AuthorityRent rent := params.AuthorityRent
@ -684,7 +684,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
k.SetNameAuthority(ctx, name, &authority) k.SetNameAuthority(ctx, name, &authority)
k.DeleteAuthorityExpiryQueue(ctx, name, authority) k.DeleteAuthorityExpiryQueue(ctx, name, authority)
ctx.Logger().Info(fmt.Sprintf("Insufficient funds in owner account to pay authority rent, marking as expired: %s", name)) k.Logger(ctx).Info(fmt.Sprintf("Insufficient funds in owner account to pay authority rent, marking as expired: %s", name))
return return
} }
@ -699,7 +699,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
k.SetNameAuthority(ctx, name, &authority) k.SetNameAuthority(ctx, name, &authority)
k.AddBondToAuthorityIndexEntry(ctx, authority.BondId, name) k.AddBondToAuthorityIndexEntry(ctx, authority.BondId, name)
ctx.Logger().Info(fmt.Sprintf("Authority rent paid successfully: %s", name)) k.Logger(ctx).Info(fmt.Sprintf("Authority rent paid successfully: %s", name))
} }
// ListNameAuthorityRecords - get all name authority records. // ListNameAuthorityRecords - get all name authority records.

View File

@ -39,14 +39,14 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
name := k.GetAuctionToAuthorityMapping(ctx, auctionID) name := k.GetAuctionToAuthorityMapping(ctx, auctionID)
if name == "" { if name == "" {
// We don't know about this auction, ignore. // We don't know about this auction, ignore.
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, name mapping not found: %s", auctionID)) logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, name mapping not found: %s", auctionID))
return return
} }
store := ctx.KVStore(k.storeKey) store := ctx.KVStore(k.storeKey)
if !HasNameAuthority(store, name) { if !HasNameAuthority(store, name) {
// We don't know about this authority, ignore. // We don't know about this authority, ignore.
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, authority not found: %s", auctionID)) logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, authority not found: %s", auctionID))
return return
} }
@ -71,12 +71,12 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
// Can be used to check if names are older than the authority itself (stale names). // Can be used to check if names are older than the authority itself (stale names).
authority.Height = uint64(ctx.BlockHeight()) authority.Height = uint64(ctx.BlockHeight())
ctx.Logger().Info(fmt.Sprintf("Winner selected, marking authority as active: %s", name)) logger(ctx).Info(fmt.Sprintf("Winner selected, marking authority as active: %s", name))
} else { } else {
// Mark as expired. // Mark as expired.
authority.Status = types.AuthorityExpired authority.Status = types.AuthorityExpired
ctx.Logger().Info(fmt.Sprintf("No winner, marking authority as expired: %s", name)) logger(ctx).Info(fmt.Sprintf("No winner, marking authority as expired: %s", name))
} }
authority.AuctionId = "" authority.AuctionId = ""
@ -85,7 +85,7 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
// Forget about this auction now, we no longer need it. // Forget about this auction now, we no longer need it.
removeAuctionToAuthorityMapping(store, auctionID) removeAuctionToAuthorityMapping(store, auctionID)
} else { } else {
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, status: %s", auctionObj.Status)) logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, status: %s", auctionObj.Status))
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import (
"crypto/sha256" "crypto/sha256"
"github.com/cerc-io/laconicd/x/registry/helpers" "github.com/cerc-io/laconicd/x/registry/helpers"
canonicalJson "github.com/gibson042/canonicaljson-go" "github.com/gibson042/canonicaljson-go"
) )
const ( const (
@ -13,10 +13,16 @@ const (
AuthorityUnderAuction = "auction" AuthorityUnderAuction = "auction"
) )
// TODO if schema records are to be more permissive than allowing a map of fields, this type will
// become specific to content records. schema records will either occupy a new message or have new
// more general purpose helper types.
type AttributeMap map[string]interface{}
// ReadablePayload represents a signed record payload that can be serialized from/to YAML. // ReadablePayload represents a signed record payload that can be serialized from/to YAML.
type ReadablePayload struct { type ReadablePayload struct {
Record map[string]interface{} `json:"record"` RecordAttributes AttributeMap `json:"record" yaml:"record"`
Signatures []Signature `json:"signatures"` Signatures []Signature `json:"signatures" yaml:"signatures"`
} }
// ReadableRecord represents a WNS record. // ReadableRecord represents a WNS record.
@ -28,23 +34,22 @@ type ReadableRecord struct {
ExpiryTime string `json:"expiryTime,omitempty"` ExpiryTime string `json:"expiryTime,omitempty"`
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
Owners []string `json:"owners,omitempty"` Owners []string `json:"owners,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"` Attributes AttributeMap `json:"attributes,omitempty"`
} }
// ToPayload converts PayloadEncodable to Payload object. // ToPayload converts PayloadEncodable to Payload object.
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4. // Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
func (payloadObj *ReadablePayload) ToPayload() Payload { func (payloadObj *ReadablePayload) ToPayload() Payload {
// Note: record directly contains the attributes here // Note: record directly contains the attributes here
attributes := helpers.MarshalMapToJSONBytes(payloadObj.Record) attributes := payloadObj.RecordAttributes
payload := Payload{ payload := Payload{
Record: &Record{ Record: &Record{
Deleted: false, Deleted: false,
Owners: nil, Owners: nil,
Attributes: attributes, Attributes: helpers.MustMarshalJSON(attributes),
}, },
Signatures: payloadObj.Signatures, Signatures: payloadObj.Signatures,
} }
// TODO rm error
return payload return payload
} }
@ -52,12 +57,28 @@ func (payloadObj *ReadablePayload) ToPayload() Payload {
func (payload Payload) ToReadablePayload() ReadablePayload { func (payload Payload) ToReadablePayload() ReadablePayload {
var encodable ReadablePayload var encodable ReadablePayload
encodable.Record = helpers.UnMarshalMapFromJSONBytes(payload.Record.Attributes) encodable.RecordAttributes = helpers.MustUnmarshalJSON[AttributeMap](payload.Record.Attributes)
encodable.Signatures = payload.Signatures encodable.Signatures = payload.Signatures
return encodable return encodable
} }
// ToRecordObj converts Record to RecordObj.
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
func (r *ReadableRecord) ToRecordObj() (Record, error) {
var resourceObj Record
resourceObj.Id = r.ID
resourceObj.BondId = r.BondID
resourceObj.CreateTime = r.CreateTime
resourceObj.ExpiryTime = r.ExpiryTime
resourceObj.Deleted = r.Deleted
resourceObj.Owners = r.Owners
resourceObj.Attributes = helpers.MustMarshalJSON(r.Attributes)
return resourceObj, nil
}
// ToReadableRecord converts Record to a serializable object // ToReadableRecord converts Record to a serializable object
func (r *Record) ToReadableRecord() ReadableRecord { func (r *Record) ToReadableRecord() ReadableRecord {
var resourceObj ReadableRecord var resourceObj ReadableRecord
@ -69,34 +90,16 @@ func (r *Record) ToReadableRecord() ReadableRecord {
resourceObj.Deleted = r.Deleted resourceObj.Deleted = r.Deleted
resourceObj.Owners = r.Owners resourceObj.Owners = r.Owners
resourceObj.Names = r.Names resourceObj.Names = r.Names
resourceObj.Attributes = helpers.UnMarshalMapFromJSONBytes(r.Attributes) resourceObj.Attributes = helpers.MustUnmarshalJSON[AttributeMap](r.Attributes)
return resourceObj return resourceObj
} }
// ToRecordObj converts Record to RecordObj.
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
func (r *ReadableRecord) ToRecordObj() (Record, error) {
attributes := helpers.MarshalMapToJSONBytes(r.Attributes)
var resourceObj Record
resourceObj.Id = r.ID
resourceObj.BondId = r.BondID
resourceObj.CreateTime = r.CreateTime
resourceObj.ExpiryTime = r.ExpiryTime
resourceObj.Deleted = r.Deleted
resourceObj.Owners = r.Owners
resourceObj.Attributes = attributes
return resourceObj, nil
}
// CanonicalJSON returns the canonical JSON representation of the record. // CanonicalJSON returns the canonical JSON representation of the record.
func (r *ReadableRecord) CanonicalJSON() []byte { func (r *ReadableRecord) CanonicalJSON() []byte {
bytes, err := canonicalJson.Marshal(r.Attributes) bytes, err := canonicaljson.Marshal(r.Attributes)
if err != nil { if err != nil {
panic("Record marshal error.") panic("error marshaling record: " + err.Error())
} }
return bytes return bytes