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.
type Reference {
id: String! # ID of linked record.
}
# Reference to another record.
input ReferenceInput {
id: String!
}
scalar Link
# Bonds contain funds that are used to pay rent on record registration and renewal.
type Bond {
@ -37,44 +30,71 @@ type Account {
balance: [Coin!] # Current balance for each coin type.
}
# Value of a given type.
type Value {
null: Boolean
# Value describes a DAG-JSON compatible value.
union Value =
BooleanValue
| IntValue
| FloatValue
| StringValue
| BytesValue
| LinkValue
| ArrayValue
| MapValue
int: Int
float: Float
string: String
boolean: Boolean
json: String
reference: Reference
values: [Value]
type BooleanValue {
value: Boolean!
}
# Value of a given type used as input to queries.
input ValueInput {
null: Boolean
int: Int
float: Float
string: String
boolean: Boolean
type IntValue {
value: Int!
}
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.
type KeyValue {
type Attribute {
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.
input KeyValueInput {
key: String!
value: ValueInput!
value: ValueInput
}
# Status information about a node (https://docs.tendermint.com/master/rpc/#/Info/status).
@ -155,7 +175,7 @@ type Record {
createTime: String! # Record create time.
expiryTime: String! # Record expiry time.
owners: [String!] # Addresses of record owners.
attributes: [KeyValue] # Record attributes.
attributes: [Attribute!] # Record attributes.
references: [Record] # Record references.
}
@ -195,7 +215,7 @@ type Query {
getBondsByIds(ids: [String!]): [Bond]
# Query bonds.
queryBonds(attributes: [KeyValueInput]): [Bond]
queryBonds(attributes: [KeyValueInput!]): [Bond]
# Query bonds by owner.
queryBondsByOwner(ownerAddresses: [String!]): [OwnerBonds]
@ -210,7 +230,7 @@ type Query {
# Query records.
queryRecords(
# Multiple attribute conditions are in a logical AND.
attributes: [KeyValueInput]
attributes: [KeyValueInput!]
# Whether to query all records, not just named ones (false by default).
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/
# for detailed .gqlgen.yml documentation.
@ -12,3 +10,8 @@ model:
resolver:
filename: resolver.go
type: Resolver
models:
Link:
model:
- github.com/cerc-io/laconicd/gql.Link

View File

@ -2,6 +2,10 @@
package gql
type Value interface {
IsValue()
}
type Account struct {
Address string `json:"address"`
PubKey *string `json:"pubKey"`
@ -10,6 +14,17 @@ type Account struct {
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 {
ID string `json:"id"`
Status string `json:"status"`
@ -53,21 +68,52 @@ type Bond struct {
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 string `json:"type"`
Quantity string `json:"quantity"`
}
type KeyValue struct {
Key string `json:"key"`
Value *Value `json:"value"`
type FloatValue struct {
Value float64 `json:"value"`
}
func (FloatValue) IsValue() {}
type IntValue struct {
Value int `json:"value"`
}
func (IntValue) IsValue() {}
type KeyValueInput struct {
Key string `json:"key"`
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 {
Latest *NameRecordEntry `json:"latest"`
History []*NameRecordEntry `json:"history"`
@ -96,22 +142,14 @@ type PeerInfo struct {
}
type Record struct {
ID string `json:"id"`
Names []string `json:"names"`
BondID string `json:"bondId"`
CreateTime string `json:"createTime"`
ExpiryTime string `json:"expiryTime"`
Owners []string `json:"owners"`
Attributes []*KeyValue `json:"attributes"`
References []*Record `json:"references"`
}
type Reference struct {
ID string `json:"id"`
}
type ReferenceInput struct {
ID string `json:"id"`
ID string `json:"id"`
Names []string `json:"names"`
BondID string `json:"bondId"`
CreateTime string `json:"createTime"`
ExpiryTime string `json:"expiryTime"`
Owners []string `json:"owners"`
Attributes []*Attribute `json:"attributes"`
References []*Record `json:"references"`
}
type Status struct {
@ -125,6 +163,12 @@ type Status struct {
DiskUsage string `json:"disk_usage"`
}
type StringValue struct {
Value string `json:"value"`
}
func (StringValue) IsValue() {}
type SyncInfo struct {
LatestBlockHash string `json:"latest_block_hash"`
LatestBlockHeight string `json:"latest_block_height"`
@ -138,23 +182,12 @@ type ValidatorInfo struct {
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 {
Null *bool `json:"null"`
Int *int `json:"int"`
Float *float64 `json:"float"`
String *string `json:"string"`
Boolean *bool `json:"boolean"`
Reference *ReferenceInput `json:"reference"`
Values []*ValueInput `json:"values"`
Int *int `json:"int"`
Float *float64 `json:"float"`
String *string `json:"string"`
Boolean *bool `json:"boolean"`
Link *Link `json:"link"`
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(
context.Background(),
&registrytypes.QueryListRecordsRequest{
Attributes: parseRequestAttributes(attributes),
Attributes: toRPCAttributes(attributes),
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 (
"context"
"encoding/json"
"fmt"
"reflect" // #nosec G702
"fmt" // #nosec G702
"strconv"
auctiontypes "github.com/cerc-io/laconicd/x/auction/types"
bondtypes "github.com/cerc-io/laconicd/x/bond/types"
registrytypes "github.com/cerc-io/laconicd/x/registry/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.
@ -61,13 +61,21 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
return nil, nil
}
recordType := record.ToReadableRecord()
attributes, err := getAttributes(&recordType)
node, err := ipld.Decode(record.Attributes, dagjson.Decode)
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 {
return nil, err
}
references, err := getReferences(ctx, resolver, &recordType)
references, err := resolver.GetRecordsByIds(ctx, links)
if err != nil {
return nil, err
}
@ -79,11 +87,96 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
ExpiryTime: record.GetExpiryTime(),
Owners: record.GetOwners(),
Names: record.GetNames(),
Attributes: attributes,
Attributes: attributes.(MapValue).Value,
References: references,
}, 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) {
if record == nil {
return nil, fmt.Errorf("got nil record")
@ -163,136 +256,47 @@ func GetGQLAuction(auction *auctiontypes.Auction, bids []*auctiontypes.Bid) (*Au
return &gqlAuction, nil
}
func getReferences(ctx context.Context, resolver QueryResolver, r *registrytypes.ReadableRecord) ([]*Record, error) {
var ids []string
func toRPCValue(value *ValueInput) *registrytypes.QueryListRecordsRequest_ValueInput {
var rpcval registrytypes.QueryListRecordsRequest_ValueInput
// #nosec G705
for key := range r.Attributes {
//nolint: all
switch r.Attributes[key].(type) {
case interface{}:
if obj, ok := r.Attributes[key].(map[string]interface{}); ok {
if _, ok := obj["/"]; ok && len(obj) == 1 {
if _, ok := obj["/"].(string); ok {
ids = append(ids, obj["/"].(string))
}
}
}
switch {
case value == nil:
return nil
case value.Int != nil:
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Int{Int: int64(*value.Int)}
case value.Float != nil:
rpcval.Value = &registrytypes.QueryListRecordsRequest_ValueInput_Float{Float: *value.Float}
case value.String != nil:
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 resolver.GetRecordsByIds(ctx, ids)
return &rpcval
}
func getAttributes(r *registrytypes.ReadableRecord) ([]*KeyValue, error) {
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 {
func toRPCAttributes(attrs []*KeyValueInput) []*registrytypes.QueryListRecordsRequest_KeyValueInput {
kvPairs := []*registrytypes.QueryListRecordsRequest_KeyValueInput{}
for _, value := range attrs {
parsedValue := toRPCValue(value.Value)
kvPair := &registrytypes.QueryListRecordsRequest_KeyValueInput{
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)
}

View File

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

View File

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

View File

@ -3,12 +3,13 @@ package keeper_test
import (
"context"
"fmt"
"os"
"reflect"
"github.com/cerc-io/laconicd/x/registry/client/cli"
"github.com/cerc-io/laconicd/x/registry/helpers"
"github.com/cerc-io/laconicd/x/registry/keeper"
registrytypes "github.com/cerc-io/laconicd/x/registry/types"
"os"
"reflect"
)
func (suite *KeeperTestSuite) TestGrpcQueryParams() {
@ -69,26 +70,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{
Key: "type",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string",
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",
Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"WebsiteRegistrationRecord"},
},
},
},
@ -98,6 +80,35 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
false,
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)",
&registrytypes.QueryListRecordsRequest{
@ -105,8 +116,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{
Key: "tags",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string",
String_: "NOEXIST",
Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"NOEXIST"},
},
},
},
@ -123,8 +133,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{
Key: "typ",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string",
String_: "eWebsiteRegistrationRecord",
Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"eWebsiteRegistrationRecord"},
},
},
},
@ -141,8 +150,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
{
Key: "x500state_name",
Value: &registrytypes.QueryListRecordsRequest_ValueInput{
Type: "string",
String_: "california",
Value: &registrytypes.QueryListRecordsRequest_ValueInput_String_{"california"},
},
},
},
@ -183,9 +191,13 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
sr.Equal(resp.GetRecords()[0].GetBondId(), suite.bond.GetId())
for _, record := range resp.GetRecords() {
recAttr := helpers.UnMarshalMapFromJSONBytes(record.Attributes)
recAttr := helpers.MustUnmarshalJSON[registrytypes.AttributeMap](record.Attributes)
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] &&
reflect.Slice == reflect.TypeOf(recAttr[attr.Key]).Kind() &&
reflect.Slice != reflect.TypeOf(av).Kind() {

View File

@ -8,10 +8,6 @@ import (
"time"
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/legacy"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
@ -20,7 +16,18 @@ import (
auth "github.com/cosmos/cosmos-sdk/x/auth/keeper"
bank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
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"
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 (
@ -99,6 +106,10 @@ func NewKeeper(cdc codec.BinaryCodec, accountKeeper auth.AccountKeeper, bankKeep
// Logger returns a module-specific 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)
}
@ -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) {
resultRecordIds := []string{}
for i, attr := range attributes {
val := GetAttributeValue(attr.Value)
attributeIndex := GetAttributesIndexKey(attr.Key, val)
suffix, err := QueryValueToJSON(attr.Value)
if err != nil {
return nil, err
}
attributeIndex := GetAttributesIndexKey(attr.Key, suffix)
recordIds, err := k.GetAttributeMapping(ctx, attributeIndex)
if err != nil {
return nil, err
@ -175,23 +189,53 @@ func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.Query
return records, nil
}
func GetAttributeValue(input *types.QueryListRecordsRequest_ValueInput) interface{} {
if input.Type == "int" {
return input.GetInt()
// TODO not recursive, and only should be if we want to support querying with whole sub-objects,
// which seems unnecessary.
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
}
case *types.QueryListRecordsRequest_ValueInput_Int:
err := nb.AssignInt(value.Int)
if err != nil {
return nil, err
}
case *types.QueryListRecordsRequest_ValueInput_Float:
err := nb.AssignFloat(value.Float)
if err != nil {
return nil, err
}
case *types.QueryListRecordsRequest_ValueInput_Boolean:
err := nb.AssignBool(value.Boolean)
if err != nil {
return nil, err
}
case *types.QueryListRecordsRequest_ValueInput_Link:
link := cidlink.Link{Cid: cid.MustParse(value.Link)}
err := nb.AssignLink(link)
if err != nil {
return nil, err
}
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)
}
if input.Type == "float" {
return input.GetFloat()
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)
}
if input.Type == "string" {
return input.GetString_()
}
if input.Type == "boolean" {
return input.GetBoolean()
}
if input.Type == "reference" {
return input.GetReference().GetId()
}
return nil
return buf.Bytes(), nil
}
func getIntersection(a []string, b []string) []string {
@ -244,7 +288,7 @@ func (k Keeper) GetRecordExpiryQueue(ctx sdk.Context) []*types.ExpiryQueueRecord
// ProcessSetRecord creates a record.
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.ReadableRecord, error) {
payload := msg.Payload.ToReadablePayload()
record := types.ReadableRecord{Attributes: payload.Record, BondID: msg.BondId}
record := types.ReadableRecord{Attributes: payload.RecordAttributes, BondID: msg.BondId}
// Check signatures.
resourceSignBytes, _ := record.GetSignBytes()
@ -264,14 +308,12 @@ func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*type
for _, sig := range payload.Signatures {
pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey))
if err != nil {
fmt.Println("Error decoding pubKey from bytes: ", err)
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid public key.")
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err))
}
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
if !sigOK {
fmt.Println("Signature mismatch: ", sig.PubKey)
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid signature.")
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey))
}
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
}
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, record.ExpiryTime)
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime))
if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil {
return err
}
@ -334,14 +376,51 @@ func (k Keeper) PutRecord(ctx sdk.Context, record types.Record) {
k.updateBlockChangeSetForRecord(ctx, record.Id)
}
func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id string, prefix string) error {
for key, value := range attrs {
if subRecord, ok := value.(map[string]any); ok {
err := k.processAttributes(ctx, subRecord, id, key)
func (k Keeper) processAttributes(ctx sdk.Context, attrs types.AttributeMap, id string, prefix string) error {
np := basicnode.Prototype.Map
nb := np.NewBuilder()
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 {
return err
}
} else {
var buf bytes.Buffer
if err := dagjson.Encode(valuenode, &buf); err != nil {
return err
}
value := buf.Bytes()
indexKey := GetAttributesIndexKey(prefix+key, value)
if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil {
return err
@ -351,8 +430,8 @@ func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id stri
return nil
}
func GetAttributesIndexKey(key string, value interface{}) []byte {
keyString := fmt.Sprintf("%s=%s", key, value)
func GetAttributesIndexKey(key string, suffix []byte) []byte {
keyString := fmt.Sprintf("%s=%s", key, suffix)
return append(PrefixAttributesIndex, []byte(keyString)...)
}
@ -364,8 +443,6 @@ func (k Keeper) SetAttributeMapping(ctx sdk.Context, key []byte, recordID string
if err != nil {
return fmt.Errorf("cannot unmarshal byte array, error, %w", err)
}
} else {
recordIds = []string{}
}
recordIds = append(recordIds, recordID)
bz, err := json.Marshal(recordIds)

View File

@ -611,7 +611,7 @@ func (k Keeper) ProcessAuthorityExpiryQueue(ctx sdk.Context) {
k.SetNameAuthority(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
}
@ -672,7 +672,7 @@ func (k Keeper) AuthorityExpiryQueueIterator(ctx sdk.Context, endTime time.Time)
// TryTakeAuthorityRent tries to take rent from the authority bond.
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)
rent := params.AuthorityRent
@ -684,7 +684,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
k.SetNameAuthority(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
}
@ -699,7 +699,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
k.SetNameAuthority(ctx, name, &authority)
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.

View File

@ -39,14 +39,14 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
name := k.GetAuctionToAuthorityMapping(ctx, auctionID)
if name == "" {
// 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
}
store := ctx.KVStore(k.storeKey)
if !HasNameAuthority(store, name) {
// 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
}
@ -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).
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 {
// Mark as expired.
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 = ""
@ -85,7 +85,7 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
// Forget about this auction now, we no longer need it.
removeAuctionToAuthorityMapping(store, auctionID)
} 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"
"github.com/cerc-io/laconicd/x/registry/helpers"
canonicalJson "github.com/gibson042/canonicaljson-go"
"github.com/gibson042/canonicaljson-go"
)
const (
@ -13,38 +13,43 @@ const (
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.
type ReadablePayload struct {
Record map[string]interface{} `json:"record"`
Signatures []Signature `json:"signatures"`
RecordAttributes AttributeMap `json:"record" yaml:"record"`
Signatures []Signature `json:"signatures" yaml:"signatures"`
}
// ReadableRecord represents a WNS record.
type ReadableRecord struct {
ID string `json:"id,omitempty"`
Names []string `json:"names,omitempty"`
BondID string `json:"bondId,omitempty"`
CreateTime string `json:"createTime,omitempty"`
ExpiryTime string `json:"expiryTime,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Owners []string `json:"owners,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
ID string `json:"id,omitempty"`
Names []string `json:"names,omitempty"`
BondID string `json:"bondId,omitempty"`
CreateTime string `json:"createTime,omitempty"`
ExpiryTime string `json:"expiryTime,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Owners []string `json:"owners,omitempty"`
Attributes AttributeMap `json:"attributes,omitempty"`
}
// ToPayload converts PayloadEncodable to Payload object.
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
func (payloadObj *ReadablePayload) ToPayload() Payload {
// Note: record directly contains the attributes here
attributes := helpers.MarshalMapToJSONBytes(payloadObj.Record)
attributes := payloadObj.RecordAttributes
payload := Payload{
Record: &Record{
Deleted: false,
Owners: nil,
Attributes: attributes,
Attributes: helpers.MustMarshalJSON(attributes),
},
Signatures: payloadObj.Signatures,
}
// TODO rm error
return payload
}
@ -52,12 +57,28 @@ func (payloadObj *ReadablePayload) ToPayload() Payload {
func (payload Payload) ToReadablePayload() ReadablePayload {
var encodable ReadablePayload
encodable.Record = helpers.UnMarshalMapFromJSONBytes(payload.Record.Attributes)
encodable.RecordAttributes = helpers.MustUnmarshalJSON[AttributeMap](payload.Record.Attributes)
encodable.Signatures = payload.Signatures
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
func (r *Record) ToReadableRecord() ReadableRecord {
var resourceObj ReadableRecord
@ -69,34 +90,16 @@ func (r *Record) ToReadableRecord() ReadableRecord {
resourceObj.Deleted = r.Deleted
resourceObj.Owners = r.Owners
resourceObj.Names = r.Names
resourceObj.Attributes = helpers.UnMarshalMapFromJSONBytes(r.Attributes)
resourceObj.Attributes = helpers.MustUnmarshalJSON[AttributeMap](r.Attributes)
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.
func (r *ReadableRecord) CanonicalJSON() []byte {
bytes, err := canonicalJson.Marshal(r.Attributes)
bytes, err := canonicaljson.Marshal(r.Attributes)
if err != nil {
panic("Record marshal error.")
panic("error marshaling record: " + err.Error())
}
return bytes