From 538010f37786af71a71c2214763ce1629b290886 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 15 Feb 2024 15:54:36 +0530 Subject: [PATCH] Add a command to set record with keeper placeholders --- go.mod | 1 + go.sum | 2 + x/bond/keeper/keeper.go | 35 ++++ x/bond/keeper/query_server.go | 4 +- x/registry/client/cli/tx.go | 91 +++++++++ x/registry/codec.go | 22 ++- x/registry/events.go | 25 +++ x/registry/helpers/helpers.go | 133 ++++++++++++++ x/registry/keeper/keeper.go | 209 ++++++++++++++++++++- x/registry/keeper/msg_server.go | 285 ++++++++++++++++++++++++++++- x/registry/keeper/naming_keeper.go | 26 +++ x/registry/keeper/params.go | 20 ++ x/registry/keeper/query_server.go | 23 ++- x/registry/keeper/record_keeper.go | 27 +++ x/registry/keys.go | 3 + x/registry/module/autocli.go | 14 +- x/registry/module/module.go | 9 +- x/registry/msgs.go | 35 ++++ x/registry/types.go | 137 ++++++++++++++ 19 files changed, 1072 insertions(+), 29 deletions(-) create mode 100644 x/registry/client/cli/tx.go create mode 100644 x/registry/events.go create mode 100644 x/registry/helpers/helpers.go create mode 100644 x/registry/keeper/naming_keeper.go create mode 100644 x/registry/keeper/params.go create mode 100644 x/registry/msgs.go create mode 100644 x/registry/types.go diff --git a/go.mod b/go.mod index fff27d9c..48167202 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/cosmos/cosmos-sdk v0.50.3 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.4.11 + github.com/deckarep/golang-set v1.8.0 github.com/gibson042/canonicaljson-go v1.0.3 github.com/golang/protobuf v1.5.3 github.com/grpc-ecosystem/grpc-gateway v1.16.0 diff --git a/go.sum b/go.sum index 93472ff7..86161350 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= diff --git a/x/bond/keeper/keeper.go b/x/bond/keeper/keeper.go index 2d2ff846..ae5ae6d2 100644 --- a/x/bond/keeper/keeper.go +++ b/x/bond/keeper/keeper.go @@ -360,3 +360,38 @@ func (k Keeper) getMaxBondAmount(ctx sdk.Context) (sdk.Coins, error) { maxBondAmount := params.MaxBondAmount return sdk.NewCoins(maxBondAmount), nil } + +// TransferCoinsToModuleAccount moves funds from the bonds module account to another module account. +func (k Keeper) TransferCoinsToModuleAccount(ctx sdk.Context, id, moduleAccount string, coins sdk.Coins) error { + if has, err := k.HasBond(ctx, id); !has { + if err != nil { + return err + } + return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Bond not found.") + } + + bond, err := k.GetBondById(ctx, id) + if err != nil { + return err + } + + // Deduct rent from bond. + updatedBalance, isNeg := bond.Balance.SafeSub(coins...) + + if isNeg { + // Check if bond has sufficient funds. + return errorsmod.Wrap(sdkerrors.ErrInsufficientFunds, "Insufficient funds.") + } + + // Move funds from bond module to record rent module. + err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, bondtypes.ModuleName, moduleAccount, coins) + if err != nil { + return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Error transferring funds.") + } + + // Update bond balance. + bond.Balance = updatedBalance + err = k.SaveBond(ctx, &bond) + + return err +} diff --git a/x/bond/keeper/query_server.go b/x/bond/keeper/query_server.go index 992adbaf..9f9a1033 100644 --- a/x/bond/keeper/query_server.go +++ b/x/bond/keeper/query_server.go @@ -10,12 +10,12 @@ import ( bondtypes "git.vdb.to/cerc-io/laconic2d/x/bond" ) +var _ bondtypes.QueryServer = queryServer{} + type queryServer struct { k Keeper } -var _ bondtypes.QueryServer = queryServer{} - // NewQueryServerImpl returns an implementation of the module QueryServer. func NewQueryServerImpl(k Keeper) bondtypes.QueryServer { return queryServer{k} diff --git a/x/registry/client/cli/tx.go b/x/registry/client/cli/tx.go new file mode 100644 index 00000000..00178753 --- /dev/null +++ b/x/registry/client/cli/tx.go @@ -0,0 +1,91 @@ +package cli + +import ( + "os" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "gopkg.in/yaml.v2" + + "github.com/spf13/cobra" + + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" +) + +// GetTxCmd returns transaction commands for this module. +func GetTxCmd() *cobra.Command { + registryTxCmd := &cobra.Command{ + Use: registrytypes.ModuleName, + Short: "registry transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + registryTxCmd.AddCommand( + GetCmdSetRecord(), + // GetCmdRenewRecord(), + // GetCmdAssociateBond(), + // GetCmdDissociateBond(), + // GetCmdDissociateRecords(), + // GetCmdReAssociateRecords(), + // GetCmdSetName(), + // GetCmdReserveName(), + // GetCmdSetAuthorityBond(), + // GetCmdDeleteName(), + ) + + return registryTxCmd +} + +// GetCmdSetRecord is the CLI command for creating/updating a record. +func GetCmdSetRecord() *cobra.Command { + cmd := &cobra.Command{ + Use: "set [payload-file-path] [bond-id]", + Short: "Set record", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + payloadType, err := GetPayloadFromFile(args[0]) + if err != nil { + return err + } + + payload := payloadType.ToPayload() + + msg := registrytypes.NewMsgSetRecord(payload, args[1], clientCtx.GetFromAddress()) + err = msg.ValidateBasic() + if err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// GetPayloadFromFile loads payload object from YAML file. +func GetPayloadFromFile(filePath string) (*registrytypes.ReadablePayload, error) { + var payload registrytypes.ReadablePayload + + data, err := os.ReadFile(filePath) // #nosec G304 + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(data, &payload) + if err != nil { + return nil, err + } + + return &payload, nil +} diff --git a/x/registry/codec.go b/x/registry/codec.go index 0d45e6d0..5029ede5 100644 --- a/x/registry/codec.go +++ b/x/registry/codec.go @@ -7,17 +7,19 @@ import ( ) func RegisterInterfaces(registry types.InterfaceRegistry) { - registry.RegisterImplementations((*sdk.Msg)(nil)) // &MsgSetName{}, - // &MsgReserveAuthority{}, - // &MsgDeleteNameAuthority{}, - // &MsgSetAuthorityBond{}, + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgSetName{}, + &MsgReserveAuthority{}, + &MsgDeleteNameAuthority{}, + &MsgSetAuthorityBond{}, - // &MsgSetRecord{}, - // &MsgRenewRecord{}, - // &MsgAssociateBond{}, - // &MsgDissociateBond{}, - // &MsgDissociateRecords{}, - // &MsgReAssociateRecords{}, + &MsgSetRecord{}, + &MsgRenewRecord{}, + &MsgAssociateBond{}, + &MsgDissociateBond{}, + &MsgDissociateRecords{}, + &MsgReAssociateRecords{}, + ) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } diff --git a/x/registry/events.go b/x/registry/events.go new file mode 100644 index 00000000..147b3777 --- /dev/null +++ b/x/registry/events.go @@ -0,0 +1,25 @@ +package registry + +const ( + EventTypeSetRecord = "set" + EventTypeDeleteName = "delete-name" + EventTypeReserveNameAuthority = "reserve-authority" + EventTypeAuthorityBond = "authority-bond" + EventTypeRenewRecord = "renew-record" + EventTypeAssociateBond = "associate-bond" + EventTypeDissociateBond = "dissociate-bond" + EventTypeDissociateRecords = "dissociate-record" + EventTypeReAssociateRecords = "re-associate-records" + + AttributeKeySigner = "signer" + AttributeKeyOwner = "owner" + AttributeKeyBondId = "bond-id" + AttributeKeyPayload = "payload" + AttributeKeyOldBondId = "old-bond-id" + AttributeKeyNewBondId = "new-bond-id" + AttributeKeyCID = "cid" + AttributeKeyName = "name" + AttributeKeyCRN = "crn" + AttributeKeyRecordId = "record-id" + AttributeValueCategory = ModuleName +) diff --git a/x/registry/helpers/helpers.go b/x/registry/helpers/helpers.go new file mode 100644 index 00000000..2a56335a --- /dev/null +++ b/x/registry/helpers/helpers.go @@ -0,0 +1,133 @@ +package helpers + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/gob" + "encoding/hex" + "encoding/json" + "sort" + + wnsUtils "git.vdb.to/cerc-io/laconic2d/utils" + set "github.com/deckarep/golang-set" +) + +func StringToBytes(val string) []byte { + return []byte(val) +} + +func BytesToString(val []byte) string { + return string(val) +} + +func StrArrToBytesArr(val []string) ([]byte, error) { + buffer := &bytes.Buffer{} + + err := gob.NewEncoder(buffer).Encode(val) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func BytesArrToStringArr(val []byte) ([]string, error) { + buffer := bytes.NewReader(val) + var v []string + err := gob.NewDecoder(buffer).Decode(&v) + if err != nil { + return nil, err + } + return v, nil +} + +func Int64ToBytes(num int64) []byte { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.BigEndian, num) + return buf.Bytes() +} + +func MustMarshalJSON[T any](val T) (bytes []byte) { + bytes, err := json.Marshal(val) + if err != nil { + panic("JSON marshal error:" + err.Error()) + } + return +} + +func MustUnmarshalJSON[T any](bytes []byte) T { + var val T + err := json.Unmarshal(bytes, &val) + if err != nil { + panic("JSON unmarshal error:" + err.Error()) + } + return val +} + +// GetCid gets the content ID. +func GetCid(content []byte) (string, error) { + return wnsUtils.CIDFromJSONBytes(content) +} + +// BytesToBase64 encodes a byte array as a base64 string. +func BytesToBase64(bytes []byte) string { + return base64.StdEncoding.EncodeToString(bytes) +} + +// BytesFromBase64 decodes a byte array from a base64 string. +func BytesFromBase64(str string) []byte { + bytes, err := base64.StdEncoding.DecodeString(str) + if err != nil { + panic("Error decoding string to bytes.") + } + + return bytes +} + +// BytesToHex encodes a byte array as a hex string. +func BytesToHex(bytes []byte) string { + return hex.EncodeToString(bytes) +} + +// BytesFromHex decodes a byte array from a hex string. +func BytesFromHex(str string) []byte { + bytes, err := hex.DecodeString(str) + if err != nil { + panic("Error decoding hex to bytes.") + } + + return bytes +} + +func SetToSlice(set set.Set) []string { + names := []string{} + + for name := range set.Iter() { + if name, ok := name.(string); ok && name != "" { + names = append(names, name) + } + } + + sort.SliceStable(names, func(i, j int) bool { return names[i] < names[j] }) + + return names +} + +func SliceToSet(names []string) set.Set { + set := set.NewThreadUnsafeSet() + + for _, name := range names { + if name != "" { + set.Add(name) + } + } + + return set +} + +func AppendUnique(list []string, element string) []string { + set := SliceToSet(list) + set.Add(element) + + return SetToSlice(set) +} diff --git a/x/registry/keeper/keeper.go b/x/registry/keeper/keeper.go index 18b3ad17..2b606e50 100644 --- a/x/registry/keeper/keeper.go +++ b/x/registry/keeper/keeper.go @@ -1,19 +1,54 @@ package keeper import ( + "bytes" + "fmt" + "sort" + "time" + "cosmossdk.io/collections" + "cosmossdk.io/collections/indexes" storetypes "cosmossdk.io/core/store" + errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/legacy" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" auth "github.com/cosmos/cosmos-sdk/x/auth/keeper" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/gibson042/canonicaljson-go" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/node/basicnode" auctionkeeper "git.vdb.to/cerc-io/laconic2d/x/auction/keeper" bondkeeper "git.vdb.to/cerc-io/laconic2d/x/bond/keeper" registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" + "git.vdb.to/cerc-io/laconic2d/x/registry/helpers" ) // TODO: Add required methods +type RecordsIndexes struct { + BondId *indexes.Multi[string, string, registrytypes.Record] +} + +func (b RecordsIndexes) IndexesList() []collections.Index[string, registrytypes.Record] { + return []collections.Index[string, registrytypes.Record]{b.BondId} +} + +func newRecordIndexes(sb *collections.SchemaBuilder) RecordsIndexes { + return RecordsIndexes{ + BondId: indexes.NewMulti( + sb, registrytypes.BondIdIndexPrefix, "records_by_bond_id", + collections.StringKey, collections.StringKey, + func(_ string, v registrytypes.Record) (string, error) { + return v.BondId, nil + }, + ), + } +} + type Keeper struct { cdc codec.BinaryCodec @@ -24,8 +59,9 @@ type Keeper struct { auctionKeeper auctionkeeper.Keeper // state management - Schema collections.Schema - Params collections.Item[registrytypes.Params] + Schema collections.Schema + Params collections.Item[registrytypes.Params] + Records *collections.IndexedMap[string, registrytypes.Record, RecordsIndexes] } // NewKeeper creates a new Keeper instance @@ -40,8 +76,9 @@ func NewKeeper( ) Keeper { sb := collections.NewSchemaBuilder(storeService) k := Keeper{ - cdc: cdc, - Params: collections.NewItem(sb, registrytypes.ParamsPrefix, "params", codec.CollValue[registrytypes.Params](cdc)), + cdc: cdc, + Params: collections.NewItem(sb, registrytypes.ParamsPrefix, "params", codec.CollValue[registrytypes.Params](cdc)), + Records: collections.NewIndexedMap(sb, registrytypes.RecordsPrefix, "records", collections.StringKey, codec.CollValue[registrytypes.Record](cdc), newRecordIndexes(sb)), } schema, err := sb.Build() @@ -53,3 +90,167 @@ func NewKeeper( return k } + +// HasRecord - checks if a record by the given id exists. +func (k Keeper) HasRecord(ctx sdk.Context, id string) (bool, error) { + has, err := k.Records.Has(ctx, id) + if err != nil { + return false, err + } + + return has, nil +} + +// PutRecord - saves a record to the store. +func (k Keeper) SaveRecord(ctx sdk.Context, record registrytypes.Record) error { + return k.Records.Set(ctx, record.Id, record) + + // TODO + // k.updateBlockChangeSetForRecord(ctx, record.Id) +} + +// ProcessSetRecord creates a record. +func (k Keeper) SetRecord(ctx sdk.Context, msg registrytypes.MsgSetRecord) (*registrytypes.ReadableRecord, error) { + payload := msg.Payload.ToReadablePayload() + record := registrytypes.ReadableRecord{Attributes: payload.RecordAttributes, BondId: msg.BondId} + + // Check signatures. + resourceSignBytes, _ := record.GetSignBytes() + cid, err := record.GetCid() + if err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Invalid record JSON") + } + + record.Id = cid + + has, err := k.HasRecord(ctx, record.Id) + if err != nil { + return nil, err + } + if has { + // Immutable record already exists. No-op. + return &record, nil + } + + record.Owners = []string{} + for _, sig := range payload.Signatures { + pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey)) + if err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err)) + } + + sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig)) + if !sigOK { + return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey)) + } + record.Owners = append(record.Owners, pubKey.Address().String()) + } + + // Sort owners list. + sort.Strings(record.Owners) + sdkErr := k.processRecord(ctx, &record, false) + if sdkErr != nil { + return nil, sdkErr + } + + return &record, nil +} + +func (k Keeper) processRecord(ctx sdk.Context, record *registrytypes.ReadableRecord, isRenewal bool) error { + params, err := k.GetParams(ctx) + if err != nil { + return err + } + + rent := params.RecordRent + if err = k.bondKeeper.TransferCoinsToModuleAccount( + ctx, record.BondId, registrytypes.RecordRentModuleAccountName, sdk.NewCoins(rent), + ); err != nil { + return err + } + + record.CreateTime = ctx.BlockHeader().Time.Format(time.RFC3339) + record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339) + record.Deleted = false + + recordObj, err := record.ToRecordObj() + if err != nil { + return err + } + + // Save record in store. + if err = k.SaveRecord(ctx, recordObj); err != nil { + return err + } + + // TODO look up/validate record type here + + if err := k.processAttributes(ctx, record.Attributes, record.Id, ""); err != nil { + return err + } + + // TODO + // expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime)) + // if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil { + // return err + // } + + // k.InsertRecordExpiryQueue(ctx, recordObj) + + return nil +} + +func (k Keeper) processAttributes(ctx sdk.Context, attrs registrytypes.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 + } + // TODO + // value := buf.Bytes() + // indexKey := GetAttributesIndexKey(prefix+key, value) + // if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil { + // return err + // } + } + } + return nil +} diff --git a/x/registry/keeper/msg_server.go b/x/registry/keeper/msg_server.go index c43418db..6d843fff 100644 --- a/x/registry/keeper/msg_server.go +++ b/x/registry/keeper/msg_server.go @@ -1,14 +1,287 @@ package keeper -// import "git.vdb.to/cerc-io/laconic2d/x/registry" +import ( + "context" -// var _ registry.MsgServer = msgServer{} + sdk "github.com/cosmos/cosmos-sdk/types" + + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" +) + +var _ registrytypes.MsgServer = msgServer{} type msgServer struct { k Keeper } -// // NewMsgServerImpl returns an implementation of the module MsgServer interface. -// func NewMsgServerImpl(keeper Keeper) registry.MsgServer { -// return &msgServer{k: keeper} -// } +// NewMsgServerImpl returns an implementation of the module MsgServer interface. +func NewMsgServerImpl(keeper Keeper) registrytypes.MsgServer { + return &msgServer{k: keeper} +} + +func (ms msgServer) SetRecord(c context.Context, msg *registrytypes.MsgSetRecord) (*registrytypes.MsgSetRecordResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + + record, err := ms.k.SetRecord(ctx, *msg) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeSetRecord, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.GetSigner()), + sdk.NewAttribute(registrytypes.AttributeKeyBondId, msg.GetBondId()), + sdk.NewAttribute(registrytypes.AttributeKeyPayload, msg.Payload.Record.Id), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + + return ®istrytypes.MsgSetRecordResponse{Id: record.Id}, nil +} + +// nolint: all +func (ms msgServer) SetName(c context.Context, msg *registrytypes.MsgSetName) (*registrytypes.MsgSetNameResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessSetName(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeSetRecord, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyCRN, msg.Crn), + sdk.NewAttribute(registrytypes.AttributeKeyCID, msg.Cid), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgSetNameResponse{}, nil +} + +func (ms msgServer) ReserveName(c context.Context, msg *registrytypes.MsgReserveAuthority) (*registrytypes.MsgReserveAuthorityResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + _, err = sdk.AccAddressFromBech32(msg.Owner) + if err != nil { + return nil, err + } + err = ms.k.ProcessReserveAuthority(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeReserveNameAuthority, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyName, msg.Name), + sdk.NewAttribute(registrytypes.AttributeKeyOwner, msg.Owner), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgReserveAuthorityResponse{}, nil +} + +// nolint: all +func (ms msgServer) SetAuthorityBond(c context.Context, msg *registrytypes.MsgSetAuthorityBond) (*registrytypes.MsgSetAuthorityBondResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessSetAuthorityBond(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeAuthorityBond, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyName, msg.Name), + sdk.NewAttribute(registrytypes.AttributeKeyBondId, msg.BondId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgSetAuthorityBondResponse{}, nil +} + +func (ms msgServer) DeleteName(c context.Context, msg *registrytypes.MsgDeleteNameAuthority) (*registrytypes.MsgDeleteNameAuthorityResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessDeleteName(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeDeleteName, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyCRN, msg.Crn), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgDeleteNameAuthorityResponse{}, nil +} + +func (ms msgServer) RenewRecord(c context.Context, msg *registrytypes.MsgRenewRecord) (*registrytypes.MsgRenewRecordResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessRenewRecord(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeRenewRecord, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyRecordId, msg.RecordId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgRenewRecordResponse{}, nil +} + +// nolint: all +func (ms msgServer) AssociateBond(c context.Context, msg *registrytypes.MsgAssociateBond) (*registrytypes.MsgAssociateBondResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + + err = ms.k.ProcessAssociateBond(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeAssociateBond, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyRecordId, msg.RecordId), + sdk.NewAttribute(registrytypes.AttributeKeyBondId, msg.BondId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgAssociateBondResponse{}, nil +} + +func (ms msgServer) DissociateBond(c context.Context, msg *registrytypes.MsgDissociateBond) (*registrytypes.MsgDissociateBondResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessDissociateBond(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeDissociateBond, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyRecordId, msg.RecordId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgDissociateBondResponse{}, nil +} + +func (ms msgServer) DissociateRecords(c context.Context, msg *registrytypes.MsgDissociateRecords) (*registrytypes.MsgDissociateRecordsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessDissociateRecords(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeDissociateRecords, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyBondId, msg.BondId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgDissociateRecordsResponse{}, nil +} + +func (ms msgServer) ReAssociateRecords(c context.Context, msg *registrytypes.MsgReAssociateRecords) (*registrytypes.MsgReAssociateRecordsResponse, error) { //nolint: all + ctx := sdk.UnwrapSDKContext(c) + _, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + return nil, err + } + err = ms.k.ProcessReAssociateRecords(ctx, *msg) + if err != nil { + return nil, err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + registrytypes.EventTypeReAssociateRecords, + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + sdk.NewAttribute(registrytypes.AttributeKeyOldBondId, msg.OldBondId), + sdk.NewAttribute(registrytypes.AttributeKeyNewBondId, msg.NewBondId), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, registrytypes.AttributeValueCategory), + sdk.NewAttribute(registrytypes.AttributeKeySigner, msg.Signer), + ), + }) + return ®istrytypes.MsgReAssociateRecordsResponse{}, nil +} diff --git a/x/registry/keeper/naming_keeper.go b/x/registry/keeper/naming_keeper.go new file mode 100644 index 00000000..71e835e7 --- /dev/null +++ b/x/registry/keeper/naming_keeper.go @@ -0,0 +1,26 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" +) + +// ProcessSetName creates a CRN -> Record ID mapping. +func (k Keeper) ProcessSetName(ctx sdk.Context, msg registrytypes.MsgSetName) error { + panic("unimplemented") +} + +// ProcessReserveAuthority reserves a name authority. +func (k Keeper) ProcessReserveAuthority(ctx sdk.Context, msg registrytypes.MsgReserveAuthority) error { + panic("unimplemented") +} + +func (k Keeper) ProcessSetAuthorityBond(ctx sdk.Context, msg registrytypes.MsgSetAuthorityBond) error { + panic("unimplemented") +} + +// ProcessDeleteName removes a CRN -> Record ID mapping. +func (k Keeper) ProcessDeleteName(ctx sdk.Context, msg registrytypes.MsgDeleteNameAuthority) error { + panic("unimplemented") +} diff --git a/x/registry/keeper/params.go b/x/registry/keeper/params.go new file mode 100644 index 00000000..2a3be5ec --- /dev/null +++ b/x/registry/keeper/params.go @@ -0,0 +1,20 @@ +package keeper + +import ( + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GetParams - Get all parameters as types.Params. +func (k Keeper) GetParams(ctx sdk.Context) (*registrytypes.Params, error) { + params, err := k.Params.Get(ctx) + if err != nil { + return nil, err + } + + return ¶ms, nil +} + +// SetParams - set the params. +func (k Keeper) SetParams(ctx sdk.Context, params registrytypes.Params) { +} diff --git a/x/registry/keeper/query_server.go b/x/registry/keeper/query_server.go index 11895d96..5d1a440a 100644 --- a/x/registry/keeper/query_server.go +++ b/x/registry/keeper/query_server.go @@ -1,8 +1,12 @@ package keeper -// import ( -// registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" -// ) +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" +) // var _ registrytypes.QueryServer = queryServer{} @@ -14,3 +18,16 @@ type queryServer struct { // func NewQueryServerImpl(k Keeper) registrytypes.QueryServer { // return queryServer{k} // } + +func (qs queryServer) Params(c context.Context, _ *registrytypes.QueryParamsRequest) (*registrytypes.QueryParamsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + params, err := qs.k.GetParams(ctx) + if err != nil { + return nil, err + } + + return ®istrytypes.QueryParamsResponse{Params: params}, nil +} + +// TODO: Add remaining methods diff --git a/x/registry/keeper/record_keeper.go b/x/registry/keeper/record_keeper.go index ce8dcfd5..27cadb8f 100644 --- a/x/registry/keeper/record_keeper.go +++ b/x/registry/keeper/record_keeper.go @@ -2,8 +2,10 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" auctionkeeper "git.vdb.to/cerc-io/laconic2d/x/auction/keeper" + registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" ) // TODO: Add methods @@ -14,3 +16,28 @@ type RecordKeeper struct { auctionKeeper auctionkeeper.Keeper // storeKey storetypes.StoreKey // Unexposed key to access store from sdk.Context } + +// ProcessRenewRecord renews a record. +func (k Keeper) ProcessRenewRecord(ctx sdk.Context, msg registrytypes.MsgRenewRecord) error { + panic("unimplemented") +} + +// ProcessAssociateBond associates a record with a bond. +func (k Keeper) ProcessAssociateBond(ctx sdk.Context, msg registrytypes.MsgAssociateBond) error { + panic("unimplemented") +} + +// ProcessDissociateBond dissociates a record from its bond. +func (k Keeper) ProcessDissociateBond(ctx sdk.Context, msg registrytypes.MsgDissociateBond) error { + panic("unimplemented") +} + +// ProcessDissociateRecords dissociates all records associated with a given bond. +func (k Keeper) ProcessDissociateRecords(ctx sdk.Context, msg registrytypes.MsgDissociateRecords) error { + panic("unimplemented") +} + +// ProcessReAssociateRecords switches records from and old to new bond. +func (k Keeper) ProcessReAssociateRecords(ctx sdk.Context, msg registrytypes.MsgReAssociateRecords) error { + panic("unimplemented") +} diff --git a/x/registry/keys.go b/x/registry/keys.go index 29b88bdf..24100bc9 100644 --- a/x/registry/keys.go +++ b/x/registry/keys.go @@ -17,4 +17,7 @@ const ( var ( // ParamsKey is the prefix for params key ParamsPrefix = collections.NewPrefix(0) + + RecordsPrefix = collections.NewPrefix(1) + BondIdIndexPrefix = collections.NewPrefix(2) ) diff --git a/x/registry/module/autocli.go b/x/registry/module/autocli.go index 6e62c7ed..15ce0f3a 100644 --- a/x/registry/module/autocli.go +++ b/x/registry/module/autocli.go @@ -3,7 +3,8 @@ package module import ( autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" "cosmossdk.io/client/v2/autocli" - // registryv1 "git.vdb.to/cerc-io/laconic2d/api/cerc/registry/v1" + + registryv1 "git.vdb.to/cerc-io/laconic2d/api/cerc/registry/v1" ) var _ autocli.HasAutoCLIConfig = AppModule{} @@ -11,7 +12,14 @@ var _ autocli.HasAutoCLIConfig = AppModule{} // AutoCLIOptions implements the autocli.HasAutoCLIConfig interface. func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions { return &autocliv1.ModuleOptions{ - Query: nil, - Tx: nil, + Query: &autocliv1.ServiceCommandDescriptor{ + Service: registryv1.Query_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{}, + }, + Tx: &autocliv1.ServiceCommandDescriptor{ + Service: registryv1.Msg_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{}, + EnhanceCustomCommand: true, // Allow additional manual commands + }, } } diff --git a/x/registry/module/module.go b/x/registry/module/module.go index c8d9aa77..c373d234 100644 --- a/x/registry/module/module.go +++ b/x/registry/module/module.go @@ -7,6 +7,7 @@ import ( "cosmossdk.io/core/appmodule" gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -15,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" + "git.vdb.to/cerc-io/laconic2d/x/registry/client/cli" "git.vdb.to/cerc-io/laconic2d/x/registry/keeper" ) @@ -115,7 +117,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // RegisterServices registers a gRPC query service to respond to the module-specific gRPC queries. func (am AppModule) RegisterServices(cfg module.Configurator) { // Register servers - // registrytypes.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) + registrytypes.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) // registrytypes.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper)) } @@ -124,3 +126,8 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { func (am AppModule) EndBlock(ctx context.Context) error { return EndBlocker(ctx, am.keeper) } + +// Get the root tx command of this module +func (AppModule) GetTxCmd() *cobra.Command { + return cli.GetTxCmd() +} diff --git a/x/registry/msgs.go b/x/registry/msgs.go new file mode 100644 index 00000000..7a68ded3 --- /dev/null +++ b/x/registry/msgs.go @@ -0,0 +1,35 @@ +package registry + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// NewMsgSetRecord is the constructor function for MsgSetRecord. +func NewMsgSetRecord(payload Payload, bondId string, signer sdk.AccAddress) *MsgSetRecord { + return &MsgSetRecord{ + Payload: payload, + BondId: bondId, + Signer: signer.String(), + } +} + +func (msg MsgSetRecord) ValidateBasic() error { + if len(msg.Signer) == 0 { + return errorsmod.Wrap(sdkerrors.ErrInvalidAddress, msg.Signer) + } + + owners := msg.Payload.Record.Owners + for _, owner := range owners { + if owner == "" { + return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Record owner not set.") + } + } + + if len(msg.BondId) == 0 { + return errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Bond Id is required.") + } + + return nil +} diff --git a/x/registry/types.go b/x/registry/types.go new file mode 100644 index 00000000..1683acd3 --- /dev/null +++ b/x/registry/types.go @@ -0,0 +1,137 @@ +package registry + +import ( + "crypto/sha256" + + "github.com/gibson042/canonicaljson-go" + + "git.vdb.to/cerc-io/laconic2d/x/registry/helpers" +) + +const ( + AuthorityActive = "active" + AuthorityExpired = "expired" + 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 { + 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:"bond_id,omitempty"` + CreateTime string `json:"create_time,omitempty"` + ExpiryTime string `json:"expiry_time,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 := payloadObj.RecordAttributes + payload := Payload{ + Record: &Record{ + Deleted: false, + Owners: nil, + Attributes: helpers.MustMarshalJSON(attributes), + }, + Signatures: payloadObj.Signatures, + } + return payload +} + +// ToReadablePayload converts Payload to a serializable object +func (payload Payload) ToReadablePayload() ReadablePayload { + var encodable ReadablePayload + + 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 + + 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.Names = r.Names + resourceObj.Attributes = helpers.MustUnmarshalJSON[AttributeMap](r.Attributes) + + return resourceObj +} + +// CanonicalJSON returns the canonical JSON representation of the record. +func (r *ReadableRecord) CanonicalJSON() []byte { + bytes, err := canonicaljson.Marshal(r.Attributes) + if err != nil { + panic("error marshaling record: " + err.Error()) + } + + return bytes +} + +// GetSignBytes generates a record hash to be signed. +func (r *ReadableRecord) GetSignBytes() ([]byte, []byte) { + // Double SHA256 hash. + + // Input to the first round of hashing. + bytes := r.CanonicalJSON() + + // First round. + first := sha256.New() + first.Write(bytes) + firstHash := first.Sum(nil) + + // Second round of hashing takes as input the output of the first round. + second := sha256.New() + second.Write(firstHash) + secondHash := second.Sum(nil) + + return secondHash, bytes +} + +// GetCid gets the record CID. +func (r *ReadableRecord) GetCid() (string, error) { + id, err := helpers.GetCid(r.CanonicalJSON()) + if err != nil { + return "", err + } + + return id, nil +}